diff options
90 files changed, 5825 insertions, 2661 deletions
diff --git a/.gitattributes b/.gitattributes index ea2cc59e4..1d79e0804 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,75 @@ -data/templates/**/*.conf linguist-language=Jinja -*.tmpl linguist-language=Jinja -*.xml.i linguist-language=XML -*.xml.in linguist-language=XML +# Common settings that generally should always be used with your language specific settings + +# Auto detect text files and perform LF normalization +* text=auto + +# +# The above will handle all files NOT found below +# + +# Documents +*.md text diff=markdown +*.mdx text diff=markdown +*.adoc text +*.csv text eol=crlf +*.rst text +*.txt text + +# Graphics +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.tif binary +*.tiff binary +*.ico binary +# SVG treated as text by default. +*.svg text +# If you want to treat it as binary, +# use the following line instead. +# *.svg binary + +# Scripts +*.bash text eol=lf diff=bash +*.sh text eol=lf diff=bash + +# Serialisation +*.conf text +*.graphql text +*.j2 text +*.json text +*.rules text +*.service text +*.toml text +*.tmpl text linguist-language=Jinja +*.xml text +*.xml.i text linguist-language=XML +*.xml.in text linguist-language=XML +*.yaml text +*.yml text + +# Text files where line endings should be preserved +*.patch -text +*.diff -text + +# Python files +*.pxd text diff=python +*.py text diff=python +*.py3 text diff=python +*.pyw text diff=python +*.pyx text diff=python +*.pyz text diff=python +*.pyi text diff=python + +# Fix syntax highlighting on GitHub to allow comments +.vscode/*.json linguist-language=JSON-with-Comments + +# +# Exclude files from exporting +# + +.gitattributes export-ignore +.gitignore export-ignore +.gitkeep export-ignore +*.pyc binary export-ignore +*.pyo binary export-ignore diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..d21d151f7 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,22 @@ +name: "Issue and PR stale management" +on: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + if: github.repository == 'vyos/vyos-1x' + steps: + # Issue stale management + - uses: actions/stale@v6 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 90 + days-before-close: -1 + stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. The issue will be reviewed by a maintainer and may be closed' + stale-issue-label: 'state: stale' + exempt-issue-labels: 'state: accepted, state: in-progress' + stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. The PR will be reviewed by a maintainer and may be closed' + stale-pr-label: 'state: stale' + exempt-pr-labels: 'state: accepted, state: in-progress' diff --git a/.vscode/settings.json b/.vscode/settings.json index caa87ba4a..27ed4c296 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,26 @@ { - "files.trimTrailingWhitespace": true, - "editor.tabSize": 4, - "editor.insertSpaces": true, "files.insertFinalNewline": true, - "files.eol": "\n", - # https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + + // https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers "files.associations": { + "*.j2": "jinja", "*.xml.i": "xml", "*.xml.in": "xml", - "*.j2": "jinja", }, - "editor.indentSize": "tabSize", "[jinja]": { - "editor.tabSize": 4, - "editor.wordBasedSuggestions": false - } + "editor.wordBasedSuggestions": "off" + }, + // https://code.visualstudio.com/docs/python/settings-reference + "python.analysis.extraPaths": [ + "./python" + ], + // https://help.gitkraken.com/gitlens/gitlens-settings/#autolink-settings + "gitlens.autolinks": [ + { + "prefix": "T", + "url": "https://vyos.dev/T<num>" + } + ], } diff --git a/data/configd-include.json b/data/configd-include.json index a762a6d4c..92d3863ce 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -53,6 +53,7 @@ "protocols_rip.py", "protocols_ripng.py", "protocols_rpki.py", +"protocols_segment_routing.py", "protocols_static.py", "protocols_static_multicast.py", "qos.py", diff --git a/data/templates/container/containers.conf.j2 b/data/templates/container/containers.conf.j2 index c635ca213..c8b54dfbb 100644 --- a/data/templates/container/containers.conf.j2 +++ b/data/templates/container/containers.conf.j2 @@ -1,709 +1,709 @@ -### Autogenerated by container.py ###
-
-# The containers configuration file specifies all of the available configuration
-# command-line options/flags for container engine tools like Podman & Buildah,
-# but in a TOML format that can be easily modified and versioned.
-
-# Please refer to containers.conf(5) for details of all configuration options.
-# Not all container engines implement all of the options.
-# All of the options have hard coded defaults and these options will override
-# the built in defaults. Users can then override these options via the command
-# line. Container engines will read containers.conf files in up to three
-# locations in the following order:
-# 1. /usr/share/containers/containers.conf
-# 2. /etc/containers/containers.conf
-# 3. $HOME/.config/containers/containers.conf (Rootless containers ONLY)
-# Items specified in the latter containers.conf, if they exist, override the
-# previous containers.conf settings, or the default settings.
-
-[containers]
-
-# List of annotation. Specified as
-# "key = value"
-# If it is empty or commented out, no annotations will be added
-#
-#annotations = []
-
-# Used to change the name of the default AppArmor profile of container engine.
-#
-#apparmor_profile = "container-default"
-
-# The hosts entries from the base hosts file are added to the containers hosts
-# file. This must be either an absolute path or as special values "image" which
-# uses the hosts file from the container image or "none" which means
-# no base hosts file is used. The default is "" which will use /etc/hosts.
-#
-#base_hosts_file = ""
-
-# Default way to to create a cgroup namespace for the container
-# Options are:
-# `private` Create private Cgroup Namespace for the container.
-# `host` Share host Cgroup Namespace with the container.
-#
-#cgroupns = "private"
-
-# Control container cgroup configuration
-# Determines whether the container will create CGroups.
-# Options are:
-# `enabled` Enable cgroup support within container
-# `disabled` Disable cgroup support, will inherit cgroups from parent
-# `no-conmon` Do not create a cgroup dedicated to conmon.
-#
-#cgroups = "enabled"
-
-# List of default capabilities for containers. If it is empty or commented out,
-# the default capabilities defined in the container engine will be added.
-#
-default_capabilities = [
- "CHOWN",
- "DAC_OVERRIDE",
- "FOWNER",
- "FSETID",
- "KILL",
- "NET_BIND_SERVICE",
- "SETFCAP",
- "SETGID",
- "SETPCAP",
- "SETUID",
- "SYS_CHROOT"
-]
-
-# A list of sysctls to be set in containers by default,
-# specified as "name=value",
-# for example:"net.ipv4.ping_group_range=0 0".
-#
-default_sysctls = [
- "net.ipv4.ping_group_range=0 0",
-]
-
-# A list of ulimits to be set in containers by default, specified as
-# "<ulimit name>=<soft limit>:<hard limit>", for example:
-# "nofile=1024:2048"
-# See setrlimit(2) for a list of resource names.
-# Any limit not specified here will be inherited from the process launching the
-# container engine.
-# Ulimits has limits for non privileged container engines.
-#
-#default_ulimits = [
-# "nofile=1280:2560",
-#]
-
-# List of devices. Specified as
-# "<device-on-host>:<device-on-container>:<permissions>", for example:
-# "/dev/sdc:/dev/xvdc:rwm".
-# If it is empty or commented out, only the default devices will be used
-#
-#devices = []
-
-# List of default DNS options to be added to /etc/resolv.conf inside of the container.
-#
-#dns_options = []
-
-# List of default DNS search domains to be added to /etc/resolv.conf inside of the container.
-#
-#dns_searches = []
-
-# Set default DNS servers.
-# This option can be used to override the DNS configuration passed to the
-# container. The special value "none" can be specified to disable creation of
-# /etc/resolv.conf in the container.
-# The /etc/resolv.conf file in the image will be used without changes.
-#
-#dns_servers = []
-
-# Environment variable list for the conmon process; used for passing necessary
-# environment variables to conmon or the runtime.
-#
-#env = [
-# "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
-# "TERM=xterm",
-#]
-
-# Pass all host environment variables into the container.
-#
-#env_host = false
-
-# Set the ip for the host.containers.internal entry in the containers /etc/hosts
-# file. This can be set to "none" to disable adding this entry. By default it
-# will automatically choose the host ip.
-#
-# NOTE: When using podman machine this entry will never be added to the containers
-# hosts file instead the gvproxy dns resolver will resolve this hostname. Therefore
-# it is not possible to disable the entry in this case.
-#
-#host_containers_internal_ip = ""
-
-# Default proxy environment variables passed into the container.
-# The environment variables passed in include:
-# http_proxy, https_proxy, ftp_proxy, no_proxy, and the upper case versions of
-# these. This option is needed when host system uses a proxy but container
-# should not use proxy. Proxy environment variables specified for the container
-# in any other way will override the values passed from the host.
-#
-#http_proxy = true
-
-# Run an init inside the container that forwards signals and reaps processes.
-#
-#init = false
-
-# Container init binary, if init=true, this is the init binary to be used for containers.
-#
-#init_path = "/usr/libexec/podman/catatonit"
-
-# Default way to to create an IPC namespace (POSIX SysV IPC) for the container
-# Options are:
-# "host" Share host IPC Namespace with the container.
-# "none" Create shareable IPC Namespace for the container without a private /dev/shm.
-# "private" Create private IPC Namespace for the container, other containers are not allowed to share it.
-# "shareable" Create shareable IPC Namespace for the container.
-#
-#ipcns = "shareable"
-
-# keyring tells the container engine whether to create
-# a kernel keyring for use within the container.
-#
-#keyring = true
-
-# label tells the container engine whether to use container separation using
-# MAC(SELinux) labeling or not.
-# The label flag is ignored on label disabled systems.
-#
-#label = true
-
-# Logging driver for the container. Available options: k8s-file and journald.
-#
-#log_driver = "k8s-file"
-
-# Maximum size allowed for the container log file. Negative numbers indicate
-# that no size limit is imposed. If positive, it must be >= 8192 to match or
-# exceed conmon's read buffer. The file is truncated and re-opened so the
-# limit is never exceeded.
-#
-#log_size_max = -1
-
-# Specifies default format tag for container log messages.
-# This is useful for creating a specific tag for container log messages.
-# Containers logs default to truncated container ID as a tag.
-#
-#log_tag = ""
-
-# Default way to to create a Network namespace for the container
-# Options are:
-# `private` Create private Network Namespace for the container.
-# `host` Share host Network Namespace with the container.
-# `none` Containers do not use the network
-#
-#netns = "private"
-
-# Create /etc/hosts for the container. By default, container engine manage
-# /etc/hosts, automatically adding the container's own IP address.
-#
-#no_hosts = false
-
-# Default way to to create a PID namespace for the container
-# Options are:
-# `private` Create private PID Namespace for the container.
-# `host` Share host PID Namespace with the container.
-#
-#pidns = "private"
-
-# Maximum number of processes allowed in a container.
-#
-#pids_limit = 2048
-
-# Copy the content from the underlying image into the newly created volume
-# when the container is created instead of when it is started. If false,
-# the container engine will not copy the content until the container is started.
-# Setting it to true may have negative performance implications.
-#
-#prepare_volume_on_create = false
-
-# Path to the seccomp.json profile which is used as the default seccomp profile
-# for the runtime.
-#
-#seccomp_profile = "/usr/share/containers/seccomp.json"
-
-# Size of /dev/shm. Specified as <number><unit>.
-# Unit is optional, values:
-# b (bytes), k (kilobytes), m (megabytes), or g (gigabytes).
-# If the unit is omitted, the system uses bytes.
-#
-#shm_size = "65536k"
-
-# Set timezone in container. Takes IANA timezones as well as "local",
-# which sets the timezone in the container to match the host machine.
-#
-#tz = ""
-
-# Set umask inside the container
-#
-#umask = "0022"
-
-# Default way to to create a User namespace for the container
-# Options are:
-# `auto` Create unique User Namespace for the container.
-# `host` Share host User Namespace with the container.
-#
-#userns = "host"
-
-# Number of UIDs to allocate for the automatic container creation.
-# UIDs are allocated from the "container" UIDs listed in
-# /etc/subuid & /etc/subgid
-#
-#userns_size = 65536
-
-# Default way to to create a UTS namespace for the container
-# Options are:
-# `private` Create private UTS Namespace for the container.
-# `host` Share host UTS Namespace with the container.
-#
-#utsns = "private"
-
-# List of volumes. Specified as
-# "<directory-on-host>:<directory-in-container>:<options>", for example:
-# "/db:/var/lib/db:ro".
-# If it is empty or commented out, no volumes will be added
-#
-#volumes = []
-
-[secrets]
-#driver = "file"
-
-[secrets.opts]
-#root = "/example/directory"
-
-[network]
-
-# Network backend determines what network driver will be used to set up and tear down container networks.
-# Valid values are "cni" and "netavark".
-# The default value is empty which means that it will automatically choose CNI or netavark. If there are
-# already containers/images or CNI networks preset it will choose CNI.
-#
-# Before changing this value all containers must be stopped otherwise it is likely that
-# iptables rules and network interfaces might leak on the host. A reboot will fix this.
-#
-network_backend = "netavark"
-
-# Path to directory where CNI plugin binaries are located.
-#
-#cni_plugin_dirs = [
-# "/usr/local/libexec/cni",
-# "/usr/libexec/cni",
-# "/usr/local/lib/cni",
-# "/usr/lib/cni",
-# "/opt/cni/bin",
-#]
-
-# The network name of the default network to attach pods to.
-#
-#default_network = "podman"
-
-# The default subnet for the default network given in default_network.
-# If a network with that name does not exist, a new network using that name and
-# this subnet will be created.
-# Must be a valid IPv4 CIDR prefix.
-#
-#default_subnet = "10.88.0.0/16"
-
-# DefaultSubnetPools is a list of subnets and size which are used to
-# allocate subnets automatically for podman network create.
-# It will iterate through the list and will pick the first free subnet
-# with the given size. This is only used for ipv4 subnets, ipv6 subnets
-# are always assigned randomly.
-#
-#default_subnet_pools = [
-# {"base" = "10.89.0.0/16", "size" = 24},
-# {"base" = "10.90.0.0/15", "size" = 24},
-# {"base" = "10.92.0.0/14", "size" = 24},
-# {"base" = "10.96.0.0/11", "size" = 24},
-# {"base" = "10.128.0.0/9", "size" = 24},
-#]
-
-# Path to the directory where network configuration files are located.
-# For the CNI backend the default is "/etc/cni/net.d" as root
-# and "$HOME/.config/cni/net.d" as rootless.
-# For the netavark backend "/etc/containers/networks" is used as root
-# and "$graphroot/networks" as rootless.
-#
-#network_config_dir = "/etc/cni/net.d/"
-
-# Port to use for dns forwarding daemon with netavark in rootful bridge
-# mode and dns enabled.
-# Using an alternate port might be useful if other dns services should
-# run on the machine.
-#
-#dns_bind_port = 53
-
-[engine]
-# Index to the active service
-#
-#active_service = production
-
-# The compression format to use when pushing an image.
-# Valid options are: `gzip`, `zstd` and `zstd:chunked`.
-#
-#compression_format = "gzip"
-
-
-# Cgroup management implementation used for the runtime.
-# Valid options "systemd" or "cgroupfs"
-#
-#cgroup_manager = "systemd"
-
-# Environment variables to pass into conmon
-#
-#conmon_env_vars = [
-# "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
-#]
-
-# Paths to look for the conmon container manager binary
-#
-#conmon_path = [
-# "/usr/libexec/podman/conmon",
-# "/usr/local/libexec/podman/conmon",
-# "/usr/local/lib/podman/conmon",
-# "/usr/bin/conmon",
-# "/usr/sbin/conmon",
-# "/usr/local/bin/conmon",
-# "/usr/local/sbin/conmon"
-#]
-
-# Enforces using docker.io for completing short names in Podman's compatibility
-# REST API. Note that this will ignore unqualified-search-registries and
-# short-name aliases defined in containers-registries.conf(5).
-#compat_api_enforce_docker_hub = true
-
-# Specify the keys sequence used to detach a container.
-# Format is a single character [a-Z] or a comma separated sequence of
-# `ctrl-<value>`, where `<value>` is one of:
-# `a-z`, `@`, `^`, `[`, `\`, `]`, `^` or `_`
-#
-#detach_keys = "ctrl-p,ctrl-q"
-
-# Determines whether engine will reserve ports on the host when they are
-# forwarded to containers. When enabled, when ports are forwarded to containers,
-# ports are held open by as long as the container is running, ensuring that
-# they cannot be reused by other programs on the host. However, this can cause
-# significant memory usage if a container has many ports forwarded to it.
-# Disabling this can save memory.
-#
-#enable_port_reservation = true
-
-# Environment variables to be used when running the container engine (e.g., Podman, Buildah).
-# For example "http_proxy=internal.proxy.company.com".
-# Note these environment variables will not be used within the container.
-# Set the env section under [containers] table, if you want to set environment variables for the container.
-#
-#env = []
-
-# Define where event logs will be stored, when events_logger is "file".
-#events_logfile_path=""
-
-# Sets the maximum size for events_logfile_path.
-# The size can be b (bytes), k (kilobytes), m (megabytes), or g (gigabytes).
-# The format for the size is `<number><unit>`, e.g., `1b` or `3g`.
-# If no unit is included then the size will be read in bytes.
-# When the limit is exceeded, the logfile will be rotated and the old one will be deleted.
-# If the maximum size is set to 0, then no limit will be applied,
-# and the logfile will not be rotated.
-#events_logfile_max_size = "1m"
-
-# Selects which logging mechanism to use for container engine events.
-# Valid values are `journald`, `file` and `none`.
-#
-#events_logger = "journald"
-
-# A is a list of directories which are used to search for helper binaries.
-#
-#helper_binaries_dir = [
-# "/usr/local/libexec/podman",
-# "/usr/local/lib/podman",
-# "/usr/libexec/podman",
-# "/usr/lib/podman",
-#]
-
-# Path to OCI hooks directories for automatically executed hooks.
-#
-#hooks_dir = [
-# "/usr/share/containers/oci/hooks.d",
-#]
-
-# Manifest Type (oci, v2s2, or v2s1) to use when pulling, pushing, building
-# container images. By default image pulled and pushed match the format of the
-# source image. Building/committing defaults to OCI.
-#
-#image_default_format = ""
-
-# Default transport method for pulling and pushing for images
-#
-#image_default_transport = "docker://"
-
-# Maximum number of image layers to be copied (pulled/pushed) simultaneously.
-# Not setting this field, or setting it to zero, will fall back to containers/image defaults.
-#
-#image_parallel_copies = 0
-
-# Tells container engines how to handle the builtin image volumes.
-# * bind: An anonymous named volume will be created and mounted
-# into the container.
-# * tmpfs: The volume is mounted onto the container as a tmpfs,
-# which allows users to create content that disappears when
-# the container is stopped.
-# * ignore: All volumes are just ignored and no action is taken.
-#
-#image_volume_mode = ""
-
-# Default command to run the infra container
-#
-#infra_command = "/pause"
-
-# Infra (pause) container image name for pod infra containers. When running a
-# pod, we start a `pause` process in a container to hold open the namespaces
-# associated with the pod. This container does nothing other then sleep,
-# reserving the pods resources for the lifetime of the pod. By default container
-# engines run a builtin container using the pause executable. If you want override
-# specify an image to pull.
-#
-#infra_image = ""
-
-# Specify the locking mechanism to use; valid values are "shm" and "file".
-# Change the default only if you are sure of what you are doing, in general
-# "file" is useful only on platforms where cgo is not available for using the
-# faster "shm" lock type. You may need to run "podman system renumber" after
-# you change the lock type.
-#
-#lock_type** = "shm"
-
-# MultiImageArchive - if true, the container engine allows for storing archives
-# (e.g., of the docker-archive transport) with multiple images. By default,
-# Podman creates single-image archives.
-#
-#multi_image_archive = "false"
-
-# Default engine namespace
-# If engine is joined to a namespace, it will see only containers and pods
-# that were created in the same namespace, and will create new containers and
-# pods in that namespace.
-# The default namespace is "", which corresponds to no namespace. When no
-# namespace is set, all containers and pods are visible.
-#
-#namespace = ""
-
-# Path to the slirp4netns binary
-#
-#network_cmd_path = ""
-
-# Default options to pass to the slirp4netns binary.
-# Valid options values are:
-#
-# - allow_host_loopback=true|false: Allow the slirp4netns to reach the host loopback IP (`10.0.2.2`).
-# Default is false.
-# - mtu=MTU: Specify the MTU to use for this network. (Default is `65520`).
-# - cidr=CIDR: Specify ip range to use for this network. (Default is `10.0.2.0/24`).
-# - enable_ipv6=true|false: Enable IPv6. Default is true. (Required for `outbound_addr6`).
-# - outbound_addr=INTERFACE: Specify the outbound interface slirp should bind to (ipv4 traffic only).
-# - outbound_addr=IPv4: Specify the outbound ipv4 address slirp should bind to.
-# - outbound_addr6=INTERFACE: Specify the outbound interface slirp should bind to (ipv6 traffic only).
-# - outbound_addr6=IPv6: Specify the outbound ipv6 address slirp should bind to.
-# - port_handler=rootlesskit: Use rootlesskit for port forwarding. Default.
-# Note: Rootlesskit changes the source IP address of incoming packets to a IP address in the container
-# network namespace, usually `10.0.2.100`. If your application requires the real source IP address,
-# e.g. web server logs, use the slirp4netns port handler. The rootlesskit port handler is also used for
-# rootless containers when connected to user-defined networks.
-# - port_handler=slirp4netns: Use the slirp4netns port forwarding, it is slower than rootlesskit but
-# preserves the correct source IP address. This port handler cannot be used for user-defined networks.
-#
-#network_cmd_options = []
-
-# Whether to use chroot instead of pivot_root in the runtime
-#
-#no_pivot_root = false
-
-# Number of locks available for containers and pods.
-# If this is changed, a lock renumber must be performed (e.g. with the
-# 'podman system renumber' command).
-#
-#num_locks = 2048
-
-# Set the exit policy of the pod when the last container exits.
-#pod_exit_policy = "continue"
-
-# Whether to pull new image before running a container
-#
-#pull_policy = "missing"
-
-# Indicates whether the application should be running in remote mode. This flag modifies the
-# --remote option on container engines. Setting the flag to true will default
-# `podman --remote=true` for access to the remote Podman service.
-#
-#remote = false
-
-# Default OCI runtime
-#
-#runtime = "crun"
-
-# List of the OCI runtimes that support --format=json. When json is supported
-# engine will use it for reporting nicer errors.
-#
-#runtime_supports_json = ["crun", "runc", "kata", "runsc", "krun"]
-
-# List of the OCI runtimes that supports running containers with KVM Separation.
-#
-#runtime_supports_kvm = ["kata", "krun"]
-
-# List of the OCI runtimes that supports running containers without cgroups.
-#
-#runtime_supports_nocgroups = ["crun", "krun"]
-
-# Default location for storing temporary container image content. Can be overridden with the TMPDIR environment
-# variable. If you specify "storage", then the location of the
-# container/storage tmp directory will be used.
-# image_copy_tmp_dir="/var/tmp"
-
-# Number of seconds to wait without a connection
-# before the `podman system service` times out and exits
-#
-#service_timeout = 5
-
-# Directory for persistent engine files (database, etc)
-# By default, this will be configured relative to where the containers/storage
-# stores containers
-# Uncomment to change location from this default
-#
-#static_dir = "/var/lib/containers/storage/libpod"
-
-# Number of seconds to wait for container to exit before sending kill signal.
-#
-#stop_timeout = 10
-
-# Number of seconds to wait before exit command in API process is given to.
-# This mimics Docker's exec cleanup behaviour, where the default is 5 minutes (value is in seconds).
-#
-#exit_command_delay = 300
-
-# map of service destinations
-#
-#[service_destinations]
-# [service_destinations.production]
-# URI to access the Podman service
-# Examples:
-# rootless "unix://run/user/$UID/podman/podman.sock" (Default)
-# rootful "unix://run/podman/podman.sock (Default)
-# remote rootless ssh://engineering.lab.company.com/run/user/1000/podman/podman.sock
-# remote rootful ssh://root@10.10.1.136:22/run/podman/podman.sock
-#
-# uri = "ssh://user@production.example.com/run/user/1001/podman/podman.sock"
-# Path to file containing ssh identity key
-# identity = "~/.ssh/id_rsa"
-
-# Directory for temporary files. Must be tmpfs (wiped after reboot)
-#
-#tmp_dir = "/run/libpod"
-
-# Directory for libpod named volumes.
-# By default, this will be configured relative to where containers/storage
-# stores containers.
-# Uncomment to change location from this default.
-#
-#volume_path = "/var/lib/containers/storage/volumes"
-
-# Default timeout (in seconds) for volume plugin operations.
-# Plugins are external programs accessed via a REST API; this sets a timeout
-# for requests to that API.
-# A value of 0 is treated as no timeout.
-#volume_plugin_timeout = 5
-
-# Paths to look for a valid OCI runtime (crun, runc, kata, runsc, krun, etc)
-[engine.runtimes]
-#crun = [
-# "/usr/bin/crun",
-# "/usr/sbin/crun",
-# "/usr/local/bin/crun",
-# "/usr/local/sbin/crun",
-# "/sbin/crun",
-# "/bin/crun",
-# "/run/current-system/sw/bin/crun",
-#]
-
-#kata = [
-# "/usr/bin/kata-runtime",
-# "/usr/sbin/kata-runtime",
-# "/usr/local/bin/kata-runtime",
-# "/usr/local/sbin/kata-runtime",
-# "/sbin/kata-runtime",
-# "/bin/kata-runtime",
-# "/usr/bin/kata-qemu",
-# "/usr/bin/kata-fc",
-#]
-
-#runc = [
-# "/usr/bin/runc",
-# "/usr/sbin/runc",
-# "/usr/local/bin/runc",
-# "/usr/local/sbin/runc",
-# "/sbin/runc",
-# "/bin/runc",
-# "/usr/lib/cri-o-runc/sbin/runc",
-#]
-
-#runsc = [
-# "/usr/bin/runsc",
-# "/usr/sbin/runsc",
-# "/usr/local/bin/runsc",
-# "/usr/local/sbin/runsc",
-# "/bin/runsc",
-# "/sbin/runsc",
-# "/run/current-system/sw/bin/runsc",
-#]
-
-#krun = [
-# "/usr/bin/krun",
-# "/usr/local/bin/krun",
-#]
-
-[engine.volume_plugins]
-#testplugin = "/run/podman/plugins/test.sock"
-
-[machine]
-# Number of CPU's a machine is created with.
-#
-#cpus=1
-
-# The size of the disk in GB created when init-ing a podman-machine VM.
-#
-#disk_size=10
-
-# Default image URI when creating a new VM using `podman machine init`.
-# Options: On Linux/Mac, `testing`, `stable`, `next`. On Windows, the major
-# version of the OS (e.g `36`) for Fedora 36. For all platforms you can
-# alternatively specify a custom download URL to an image. Container engines
-# translate URIs $OS and $ARCH to the native OS and ARCH. URI
-# "https://example.com/$OS/$ARCH/foobar.ami" becomes
-# "https://example.com/linux/amd64/foobar.ami" on a Linux AMD machine.
-# The default value is `testing`.
-#
-# image = "testing"
-
-# Memory in MB a machine is created with.
-#
-#memory=2048
-
-# The username to use and create on the podman machine OS for rootless
-# container access.
-#
-#user = "core"
-
-# Host directories to be mounted as volumes into the VM by default.
-# Environment variables like $HOME as well as complete paths are supported for
-# the source and destination. An optional third field `:ro` can be used to
-# tell the container engines to mount the volume readonly.
-#
-# volumes = [
-# "$HOME:$HOME",
-#]
-
-# The [machine] table MUST be the last entry in this file.
-# (Unless another table is added)
-# TOML does not provide a way to end a table other than a further table being
-# defined, so every key hereafter will be part of [machine] and not the
-# main config.
+### Autogenerated by container.py ### + +# The containers configuration file specifies all of the available configuration +# command-line options/flags for container engine tools like Podman & Buildah, +# but in a TOML format that can be easily modified and versioned. + +# Please refer to containers.conf(5) for details of all configuration options. +# Not all container engines implement all of the options. +# All of the options have hard coded defaults and these options will override +# the built in defaults. Users can then override these options via the command +# line. Container engines will read containers.conf files in up to three +# locations in the following order: +# 1. /usr/share/containers/containers.conf +# 2. /etc/containers/containers.conf +# 3. $HOME/.config/containers/containers.conf (Rootless containers ONLY) +# Items specified in the latter containers.conf, if they exist, override the +# previous containers.conf settings, or the default settings. + +[containers] + +# List of annotation. Specified as +# "key = value" +# If it is empty or commented out, no annotations will be added +# +#annotations = [] + +# Used to change the name of the default AppArmor profile of container engine. +# +#apparmor_profile = "container-default" + +# The hosts entries from the base hosts file are added to the containers hosts +# file. This must be either an absolute path or as special values "image" which +# uses the hosts file from the container image or "none" which means +# no base hosts file is used. The default is "" which will use /etc/hosts. +# +#base_hosts_file = "" + +# Default way to to create a cgroup namespace for the container +# Options are: +# `private` Create private Cgroup Namespace for the container. +# `host` Share host Cgroup Namespace with the container. +# +#cgroupns = "private" + +# Control container cgroup configuration +# Determines whether the container will create CGroups. +# Options are: +# `enabled` Enable cgroup support within container +# `disabled` Disable cgroup support, will inherit cgroups from parent +# `no-conmon` Do not create a cgroup dedicated to conmon. +# +#cgroups = "enabled" + +# List of default capabilities for containers. If it is empty or commented out, +# the default capabilities defined in the container engine will be added. +# +default_capabilities = [ + "CHOWN", + "DAC_OVERRIDE", + "FOWNER", + "FSETID", + "KILL", + "NET_BIND_SERVICE", + "SETFCAP", + "SETGID", + "SETPCAP", + "SETUID", + "SYS_CHROOT" +] + +# A list of sysctls to be set in containers by default, +# specified as "name=value", +# for example:"net.ipv4.ping_group_range=0 0". +# +default_sysctls = [ + "net.ipv4.ping_group_range=0 0", +] + +# A list of ulimits to be set in containers by default, specified as +# "<ulimit name>=<soft limit>:<hard limit>", for example: +# "nofile=1024:2048" +# See setrlimit(2) for a list of resource names. +# Any limit not specified here will be inherited from the process launching the +# container engine. +# Ulimits has limits for non privileged container engines. +# +#default_ulimits = [ +# "nofile=1280:2560", +#] + +# List of devices. Specified as +# "<device-on-host>:<device-on-container>:<permissions>", for example: +# "/dev/sdc:/dev/xvdc:rwm". +# If it is empty or commented out, only the default devices will be used +# +#devices = [] + +# List of default DNS options to be added to /etc/resolv.conf inside of the container. +# +#dns_options = [] + +# List of default DNS search domains to be added to /etc/resolv.conf inside of the container. +# +#dns_searches = [] + +# Set default DNS servers. +# This option can be used to override the DNS configuration passed to the +# container. The special value "none" can be specified to disable creation of +# /etc/resolv.conf in the container. +# The /etc/resolv.conf file in the image will be used without changes. +# +#dns_servers = [] + +# Environment variable list for the conmon process; used for passing necessary +# environment variables to conmon or the runtime. +# +#env = [ +# "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", +# "TERM=xterm", +#] + +# Pass all host environment variables into the container. +# +#env_host = false + +# Set the ip for the host.containers.internal entry in the containers /etc/hosts +# file. This can be set to "none" to disable adding this entry. By default it +# will automatically choose the host ip. +# +# NOTE: When using podman machine this entry will never be added to the containers +# hosts file instead the gvproxy dns resolver will resolve this hostname. Therefore +# it is not possible to disable the entry in this case. +# +#host_containers_internal_ip = "" + +# Default proxy environment variables passed into the container. +# The environment variables passed in include: +# http_proxy, https_proxy, ftp_proxy, no_proxy, and the upper case versions of +# these. This option is needed when host system uses a proxy but container +# should not use proxy. Proxy environment variables specified for the container +# in any other way will override the values passed from the host. +# +#http_proxy = true + +# Run an init inside the container that forwards signals and reaps processes. +# +#init = false + +# Container init binary, if init=true, this is the init binary to be used for containers. +# +#init_path = "/usr/libexec/podman/catatonit" + +# Default way to to create an IPC namespace (POSIX SysV IPC) for the container +# Options are: +# "host" Share host IPC Namespace with the container. +# "none" Create shareable IPC Namespace for the container without a private /dev/shm. +# "private" Create private IPC Namespace for the container, other containers are not allowed to share it. +# "shareable" Create shareable IPC Namespace for the container. +# +#ipcns = "shareable" + +# keyring tells the container engine whether to create +# a kernel keyring for use within the container. +# +#keyring = true + +# label tells the container engine whether to use container separation using +# MAC(SELinux) labeling or not. +# The label flag is ignored on label disabled systems. +# +#label = true + +# Logging driver for the container. Available options: k8s-file and journald. +# +#log_driver = "k8s-file" + +# Maximum size allowed for the container log file. Negative numbers indicate +# that no size limit is imposed. If positive, it must be >= 8192 to match or +# exceed conmon's read buffer. The file is truncated and re-opened so the +# limit is never exceeded. +# +#log_size_max = -1 + +# Specifies default format tag for container log messages. +# This is useful for creating a specific tag for container log messages. +# Containers logs default to truncated container ID as a tag. +# +#log_tag = "" + +# Default way to to create a Network namespace for the container +# Options are: +# `private` Create private Network Namespace for the container. +# `host` Share host Network Namespace with the container. +# `none` Containers do not use the network +# +#netns = "private" + +# Create /etc/hosts for the container. By default, container engine manage +# /etc/hosts, automatically adding the container's own IP address. +# +#no_hosts = false + +# Default way to to create a PID namespace for the container +# Options are: +# `private` Create private PID Namespace for the container. +# `host` Share host PID Namespace with the container. +# +#pidns = "private" + +# Maximum number of processes allowed in a container. +# +#pids_limit = 2048 + +# Copy the content from the underlying image into the newly created volume +# when the container is created instead of when it is started. If false, +# the container engine will not copy the content until the container is started. +# Setting it to true may have negative performance implications. +# +#prepare_volume_on_create = false + +# Path to the seccomp.json profile which is used as the default seccomp profile +# for the runtime. +# +#seccomp_profile = "/usr/share/containers/seccomp.json" + +# Size of /dev/shm. Specified as <number><unit>. +# Unit is optional, values: +# b (bytes), k (kilobytes), m (megabytes), or g (gigabytes). +# If the unit is omitted, the system uses bytes. +# +#shm_size = "65536k" + +# Set timezone in container. Takes IANA timezones as well as "local", +# which sets the timezone in the container to match the host machine. +# +#tz = "" + +# Set umask inside the container +# +#umask = "0022" + +# Default way to to create a User namespace for the container +# Options are: +# `auto` Create unique User Namespace for the container. +# `host` Share host User Namespace with the container. +# +#userns = "host" + +# Number of UIDs to allocate for the automatic container creation. +# UIDs are allocated from the "container" UIDs listed in +# /etc/subuid & /etc/subgid +# +#userns_size = 65536 + +# Default way to to create a UTS namespace for the container +# Options are: +# `private` Create private UTS Namespace for the container. +# `host` Share host UTS Namespace with the container. +# +#utsns = "private" + +# List of volumes. Specified as +# "<directory-on-host>:<directory-in-container>:<options>", for example: +# "/db:/var/lib/db:ro". +# If it is empty or commented out, no volumes will be added +# +#volumes = [] + +[secrets] +#driver = "file" + +[secrets.opts] +#root = "/example/directory" + +[network] + +# Network backend determines what network driver will be used to set up and tear down container networks. +# Valid values are "cni" and "netavark". +# The default value is empty which means that it will automatically choose CNI or netavark. If there are +# already containers/images or CNI networks preset it will choose CNI. +# +# Before changing this value all containers must be stopped otherwise it is likely that +# iptables rules and network interfaces might leak on the host. A reboot will fix this. +# +network_backend = "netavark" + +# Path to directory where CNI plugin binaries are located. +# +#cni_plugin_dirs = [ +# "/usr/local/libexec/cni", +# "/usr/libexec/cni", +# "/usr/local/lib/cni", +# "/usr/lib/cni", +# "/opt/cni/bin", +#] + +# The network name of the default network to attach pods to. +# +#default_network = "podman" + +# The default subnet for the default network given in default_network. +# If a network with that name does not exist, a new network using that name and +# this subnet will be created. +# Must be a valid IPv4 CIDR prefix. +# +#default_subnet = "10.88.0.0/16" + +# DefaultSubnetPools is a list of subnets and size which are used to +# allocate subnets automatically for podman network create. +# It will iterate through the list and will pick the first free subnet +# with the given size. This is only used for ipv4 subnets, ipv6 subnets +# are always assigned randomly. +# +#default_subnet_pools = [ +# {"base" = "10.89.0.0/16", "size" = 24}, +# {"base" = "10.90.0.0/15", "size" = 24}, +# {"base" = "10.92.0.0/14", "size" = 24}, +# {"base" = "10.96.0.0/11", "size" = 24}, +# {"base" = "10.128.0.0/9", "size" = 24}, +#] + +# Path to the directory where network configuration files are located. +# For the CNI backend the default is "/etc/cni/net.d" as root +# and "$HOME/.config/cni/net.d" as rootless. +# For the netavark backend "/etc/containers/networks" is used as root +# and "$graphroot/networks" as rootless. +# +#network_config_dir = "/etc/cni/net.d/" + +# Port to use for dns forwarding daemon with netavark in rootful bridge +# mode and dns enabled. +# Using an alternate port might be useful if other dns services should +# run on the machine. +# +#dns_bind_port = 53 + +[engine] +# Index to the active service +# +#active_service = production + +# The compression format to use when pushing an image. +# Valid options are: `gzip`, `zstd` and `zstd:chunked`. +# +#compression_format = "gzip" + + +# Cgroup management implementation used for the runtime. +# Valid options "systemd" or "cgroupfs" +# +#cgroup_manager = "systemd" + +# Environment variables to pass into conmon +# +#conmon_env_vars = [ +# "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +#] + +# Paths to look for the conmon container manager binary +# +#conmon_path = [ +# "/usr/libexec/podman/conmon", +# "/usr/local/libexec/podman/conmon", +# "/usr/local/lib/podman/conmon", +# "/usr/bin/conmon", +# "/usr/sbin/conmon", +# "/usr/local/bin/conmon", +# "/usr/local/sbin/conmon" +#] + +# Enforces using docker.io for completing short names in Podman's compatibility +# REST API. Note that this will ignore unqualified-search-registries and +# short-name aliases defined in containers-registries.conf(5). +#compat_api_enforce_docker_hub = true + +# Specify the keys sequence used to detach a container. +# Format is a single character [a-Z] or a comma separated sequence of +# `ctrl-<value>`, where `<value>` is one of: +# `a-z`, `@`, `^`, `[`, `\`, `]`, `^` or `_` +# +#detach_keys = "ctrl-p,ctrl-q" + +# Determines whether engine will reserve ports on the host when they are +# forwarded to containers. When enabled, when ports are forwarded to containers, +# ports are held open by as long as the container is running, ensuring that +# they cannot be reused by other programs on the host. However, this can cause +# significant memory usage if a container has many ports forwarded to it. +# Disabling this can save memory. +# +#enable_port_reservation = true + +# Environment variables to be used when running the container engine (e.g., Podman, Buildah). +# For example "http_proxy=internal.proxy.company.com". +# Note these environment variables will not be used within the container. +# Set the env section under [containers] table, if you want to set environment variables for the container. +# +#env = [] + +# Define where event logs will be stored, when events_logger is "file". +#events_logfile_path="" + +# Sets the maximum size for events_logfile_path. +# The size can be b (bytes), k (kilobytes), m (megabytes), or g (gigabytes). +# The format for the size is `<number><unit>`, e.g., `1b` or `3g`. +# If no unit is included then the size will be read in bytes. +# When the limit is exceeded, the logfile will be rotated and the old one will be deleted. +# If the maximum size is set to 0, then no limit will be applied, +# and the logfile will not be rotated. +#events_logfile_max_size = "1m" + +# Selects which logging mechanism to use for container engine events. +# Valid values are `journald`, `file` and `none`. +# +#events_logger = "journald" + +# A is a list of directories which are used to search for helper binaries. +# +#helper_binaries_dir = [ +# "/usr/local/libexec/podman", +# "/usr/local/lib/podman", +# "/usr/libexec/podman", +# "/usr/lib/podman", +#] + +# Path to OCI hooks directories for automatically executed hooks. +# +#hooks_dir = [ +# "/usr/share/containers/oci/hooks.d", +#] + +# Manifest Type (oci, v2s2, or v2s1) to use when pulling, pushing, building +# container images. By default image pulled and pushed match the format of the +# source image. Building/committing defaults to OCI. +# +#image_default_format = "" + +# Default transport method for pulling and pushing for images +# +#image_default_transport = "docker://" + +# Maximum number of image layers to be copied (pulled/pushed) simultaneously. +# Not setting this field, or setting it to zero, will fall back to containers/image defaults. +# +#image_parallel_copies = 0 + +# Tells container engines how to handle the builtin image volumes. +# * bind: An anonymous named volume will be created and mounted +# into the container. +# * tmpfs: The volume is mounted onto the container as a tmpfs, +# which allows users to create content that disappears when +# the container is stopped. +# * ignore: All volumes are just ignored and no action is taken. +# +#image_volume_mode = "" + +# Default command to run the infra container +# +#infra_command = "/pause" + +# Infra (pause) container image name for pod infra containers. When running a +# pod, we start a `pause` process in a container to hold open the namespaces +# associated with the pod. This container does nothing other then sleep, +# reserving the pods resources for the lifetime of the pod. By default container +# engines run a builtin container using the pause executable. If you want override +# specify an image to pull. +# +#infra_image = "" + +# Specify the locking mechanism to use; valid values are "shm" and "file". +# Change the default only if you are sure of what you are doing, in general +# "file" is useful only on platforms where cgo is not available for using the +# faster "shm" lock type. You may need to run "podman system renumber" after +# you change the lock type. +# +#lock_type** = "shm" + +# MultiImageArchive - if true, the container engine allows for storing archives +# (e.g., of the docker-archive transport) with multiple images. By default, +# Podman creates single-image archives. +# +#multi_image_archive = "false" + +# Default engine namespace +# If engine is joined to a namespace, it will see only containers and pods +# that were created in the same namespace, and will create new containers and +# pods in that namespace. +# The default namespace is "", which corresponds to no namespace. When no +# namespace is set, all containers and pods are visible. +# +#namespace = "" + +# Path to the slirp4netns binary +# +#network_cmd_path = "" + +# Default options to pass to the slirp4netns binary. +# Valid options values are: +# +# - allow_host_loopback=true|false: Allow the slirp4netns to reach the host loopback IP (`10.0.2.2`). +# Default is false. +# - mtu=MTU: Specify the MTU to use for this network. (Default is `65520`). +# - cidr=CIDR: Specify ip range to use for this network. (Default is `10.0.2.0/24`). +# - enable_ipv6=true|false: Enable IPv6. Default is true. (Required for `outbound_addr6`). +# - outbound_addr=INTERFACE: Specify the outbound interface slirp should bind to (ipv4 traffic only). +# - outbound_addr=IPv4: Specify the outbound ipv4 address slirp should bind to. +# - outbound_addr6=INTERFACE: Specify the outbound interface slirp should bind to (ipv6 traffic only). +# - outbound_addr6=IPv6: Specify the outbound ipv6 address slirp should bind to. +# - port_handler=rootlesskit: Use rootlesskit for port forwarding. Default. +# Note: Rootlesskit changes the source IP address of incoming packets to a IP address in the container +# network namespace, usually `10.0.2.100`. If your application requires the real source IP address, +# e.g. web server logs, use the slirp4netns port handler. The rootlesskit port handler is also used for +# rootless containers when connected to user-defined networks. +# - port_handler=slirp4netns: Use the slirp4netns port forwarding, it is slower than rootlesskit but +# preserves the correct source IP address. This port handler cannot be used for user-defined networks. +# +#network_cmd_options = [] + +# Whether to use chroot instead of pivot_root in the runtime +# +#no_pivot_root = false + +# Number of locks available for containers and pods. +# If this is changed, a lock renumber must be performed (e.g. with the +# 'podman system renumber' command). +# +#num_locks = 2048 + +# Set the exit policy of the pod when the last container exits. +#pod_exit_policy = "continue" + +# Whether to pull new image before running a container +# +#pull_policy = "missing" + +# Indicates whether the application should be running in remote mode. This flag modifies the +# --remote option on container engines. Setting the flag to true will default +# `podman --remote=true` for access to the remote Podman service. +# +#remote = false + +# Default OCI runtime +# +#runtime = "crun" + +# List of the OCI runtimes that support --format=json. When json is supported +# engine will use it for reporting nicer errors. +# +#runtime_supports_json = ["crun", "runc", "kata", "runsc", "krun"] + +# List of the OCI runtimes that supports running containers with KVM Separation. +# +#runtime_supports_kvm = ["kata", "krun"] + +# List of the OCI runtimes that supports running containers without cgroups. +# +#runtime_supports_nocgroups = ["crun", "krun"] + +# Default location for storing temporary container image content. Can be overridden with the TMPDIR environment +# variable. If you specify "storage", then the location of the +# container/storage tmp directory will be used. +# image_copy_tmp_dir="/var/tmp" + +# Number of seconds to wait without a connection +# before the `podman system service` times out and exits +# +#service_timeout = 5 + +# Directory for persistent engine files (database, etc) +# By default, this will be configured relative to where the containers/storage +# stores containers +# Uncomment to change location from this default +# +#static_dir = "/var/lib/containers/storage/libpod" + +# Number of seconds to wait for container to exit before sending kill signal. +# +#stop_timeout = 10 + +# Number of seconds to wait before exit command in API process is given to. +# This mimics Docker's exec cleanup behaviour, where the default is 5 minutes (value is in seconds). +# +#exit_command_delay = 300 + +# map of service destinations +# +#[service_destinations] +# [service_destinations.production] +# URI to access the Podman service +# Examples: +# rootless "unix://run/user/$UID/podman/podman.sock" (Default) +# rootful "unix://run/podman/podman.sock (Default) +# remote rootless ssh://engineering.lab.company.com/run/user/1000/podman/podman.sock +# remote rootful ssh://root@10.10.1.136:22/run/podman/podman.sock +# +# uri = "ssh://user@production.example.com/run/user/1001/podman/podman.sock" +# Path to file containing ssh identity key +# identity = "~/.ssh/id_rsa" + +# Directory for temporary files. Must be tmpfs (wiped after reboot) +# +#tmp_dir = "/run/libpod" + +# Directory for libpod named volumes. +# By default, this will be configured relative to where containers/storage +# stores containers. +# Uncomment to change location from this default. +# +#volume_path = "/var/lib/containers/storage/volumes" + +# Default timeout (in seconds) for volume plugin operations. +# Plugins are external programs accessed via a REST API; this sets a timeout +# for requests to that API. +# A value of 0 is treated as no timeout. +#volume_plugin_timeout = 5 + +# Paths to look for a valid OCI runtime (crun, runc, kata, runsc, krun, etc) +[engine.runtimes] +#crun = [ +# "/usr/bin/crun", +# "/usr/sbin/crun", +# "/usr/local/bin/crun", +# "/usr/local/sbin/crun", +# "/sbin/crun", +# "/bin/crun", +# "/run/current-system/sw/bin/crun", +#] + +#kata = [ +# "/usr/bin/kata-runtime", +# "/usr/sbin/kata-runtime", +# "/usr/local/bin/kata-runtime", +# "/usr/local/sbin/kata-runtime", +# "/sbin/kata-runtime", +# "/bin/kata-runtime", +# "/usr/bin/kata-qemu", +# "/usr/bin/kata-fc", +#] + +#runc = [ +# "/usr/bin/runc", +# "/usr/sbin/runc", +# "/usr/local/bin/runc", +# "/usr/local/sbin/runc", +# "/sbin/runc", +# "/bin/runc", +# "/usr/lib/cri-o-runc/sbin/runc", +#] + +#runsc = [ +# "/usr/bin/runsc", +# "/usr/sbin/runsc", +# "/usr/local/bin/runsc", +# "/usr/local/sbin/runsc", +# "/bin/runsc", +# "/sbin/runsc", +# "/run/current-system/sw/bin/runsc", +#] + +#krun = [ +# "/usr/bin/krun", +# "/usr/local/bin/krun", +#] + +[engine.volume_plugins] +#testplugin = "/run/podman/plugins/test.sock" + +[machine] +# Number of CPU's a machine is created with. +# +#cpus=1 + +# The size of the disk in GB created when init-ing a podman-machine VM. +# +#disk_size=10 + +# Default image URI when creating a new VM using `podman machine init`. +# Options: On Linux/Mac, `testing`, `stable`, `next`. On Windows, the major +# version of the OS (e.g `36`) for Fedora 36. For all platforms you can +# alternatively specify a custom download URL to an image. Container engines +# translate URIs $OS and $ARCH to the native OS and ARCH. URI +# "https://example.com/$OS/$ARCH/foobar.ami" becomes +# "https://example.com/linux/amd64/foobar.ami" on a Linux AMD machine. +# The default value is `testing`. +# +# image = "testing" + +# Memory in MB a machine is created with. +# +#memory=2048 + +# The username to use and create on the podman machine OS for rootless +# container access. +# +#user = "core" + +# Host directories to be mounted as volumes into the VM by default. +# Environment variables like $HOME as well as complete paths are supported for +# the source and destination. An optional third field `:ro` can be used to +# tell the container engines to mount the volume readonly. +# +# volumes = [ +# "$HOME:$HOME", +#] + +# The [machine] table MUST be the last entry in this file. +# (Unless another table is added) +# TOML does not provide a way to end a table other than a further table being +# defined, so every key hereafter will be part of [machine] and not the +# main config. diff --git a/data/templates/dhcp-server/10-override.conf.j2 b/data/templates/dhcp-server/10-override.conf.j2 deleted file mode 100644 index 1504b6808..000000000 --- a/data/templates/dhcp-server/10-override.conf.j2 +++ /dev/null @@ -1,30 +0,0 @@ -### Autogenerated by dhcp_server.py ### -{% set lease_file = '/config/dhcpd.leases' %} -[Unit] -Description=ISC DHCP IPv4 server -Documentation=man:dhcpd(8) -RequiresMountsFor=/run -ConditionPathExists= -ConditionPathExists=/run/dhcp-server/dhcpd.conf -After= -After=vyos-router.service - -[Service] -Type=forking -WorkingDirectory= -WorkingDirectory=/run/dhcp-server -RuntimeDirectory=dhcp-server -RuntimeDirectoryPreserve=yes -Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhcpd.conf LEASE_FILE={{ lease_file }} -PIDFile=/run/dhcp-server/dhcpd.pid -ExecStartPre=/bin/sh -ec '\ -touch ${LEASE_FILE}; \ -chown dhcpd:vyattacfg ${LEASE_FILE}* ; \ -chmod 664 ${LEASE_FILE}* ; \ -/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' -ExecStart= -ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} -Restart=always - -[Install] -WantedBy=multi-user.target diff --git a/data/templates/dhcp-server/dhcpd.conf.j2 b/data/templates/dhcp-server/dhcpd.conf.j2 deleted file mode 100644 index 639526532..000000000 --- a/data/templates/dhcp-server/dhcpd.conf.j2 +++ /dev/null @@ -1,250 +0,0 @@ -### Autogenerated by dhcp_server.py ### - -# For options please consult the following website: -# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html -# -# log-facility local7; -{% if hostfile_update is vyos_defined %} -on release { - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); - set ClientIp = binary-to-ascii(10, 8, ".",leased-address); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", "", ClientIp, "", ""); -} -on expiry { - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); - set ClientIp = binary-to-ascii(10, 8, ".",leased-address); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", "", ClientIp, "", ""); -} -{% endif %} - -{{ 'use-host-decl-names on;' if host_decl_name is vyos_defined }} -ddns-update-style {{ 'interim' if dynamic_dns_update is vyos_defined else 'none' }}; -option rfc3442-static-route code 121 = array of integer 8; -option windows-static-route code 249 = array of integer 8; -option wpad-url code 252 = text; -option rfc8925-ipv6-only-preferred code 108 = unsigned integer 32; - -# Vendor specific options - Ubiquiti Networks -option space ubnt; -option ubnt.unifi-controller code 1 = ip-address; -class "ubnt" { - match if substring (option vendor-class-identifier , 0, 4) = "ubnt"; - option vendor-class-identifier "ubnt"; - vendor-option-space ubnt; -} - -{% if global_parameters is vyos_defined %} -# The following {{ global_parameters | length }} line(s) have been added as -# global-parameters in the CLI and have not been validated !!! -{% for parameter in global_parameters %} -{{ parameter }} -{% endfor %} - -{% endif %} -{% if failover is vyos_defined %} -# DHCP failover configuration -failover peer "{{ failover.name }}" { -{% if failover.status == 'primary' %} - primary; - mclt 1800; - split 128; -{% elif failover.status == 'secondary' %} - secondary; -{% endif %} - address {{ failover.source_address }}; - port 647; - peer address {{ failover.remote }}; - peer port 647; - max-response-delay 30; - max-unacked-updates 10; - load balance max seconds 3; -} -{% endif %} -{% if listen_address is vyos_defined %} - -# DHCP server serving relay subnet, we need a connector to the real world -{% for address in listen_address %} -# Connected subnet statement for listen-address {{ address }} -subnet {{ address | network_from_ipv4 }} netmask {{ address | netmask_from_ipv4 }} { } -{% endfor %} -{% endif %} - -# Shared network configration(s) -{% if shared_network_name is vyos_defined %} -{% for network, network_config in shared_network_name.items() if network_config.disable is not vyos_defined %} -shared-network {{ network }} { -{% if network_config.authoritative is vyos_defined %} - authoritative; -{% endif %} -{% if network_config.name_server is vyos_defined %} - option domain-name-servers {{ network_config.name_server | join(', ') }}; -{% endif %} -{% if network_config.domain_name is vyos_defined %} - option domain-name "{{ network_config.domain_name }}"; -{% endif %} -{% if network_config.domain_search is vyos_defined %} - option domain-search "{{ network_config.domain_search | join('", "') }}"; -{% endif %} -{% if network_config.ntp_server is vyos_defined %} - option ntp-servers {{ network_config.ntp_server | join(', ') }}; -{% endif %} -{% if network_config.ping_check is vyos_defined %} - ping-check true; -{% endif %} -{% if network_config.shared_network_parameters is vyos_defined %} - # The following {{ network_config.shared_network_parameters | length }} line(s) - # were added as shared-network-parameters in the CLI and have not been validated -{% for parameter in network_config.shared_network_parameters %} - {{ parameter }} -{% endfor %} -{% endif %} -{% if network_config.subnet is vyos_defined %} -{% for subnet, subnet_config in network_config.subnet.items() %} -{% if subnet_config.description is vyos_defined %} - # {{ subnet_config.description }} -{% endif %} - subnet {{ subnet | address_from_cidr }} netmask {{ subnet | netmask_from_cidr }} { -{% if subnet_config.name_server is vyos_defined %} - option domain-name-servers {{ subnet_config.name_server | join(', ') }}; -{% endif %} -{% if subnet_config.domain_name is vyos_defined %} - option domain-name "{{ subnet_config.domain_name }}"; -{% endif %} -{% if subnet_config.domain_search is vyos_defined %} - option domain-search "{{ subnet_config.domain_search | join('", "') }}"; -{% endif %} -{% if subnet_config.ntp_server is vyos_defined %} - option ntp-servers {{ subnet_config.ntp_server | join(', ') }}; -{% endif %} -{% if subnet_config.pop_server is vyos_defined %} - option pop-server {{ subnet_config.pop_server | join(', ') }}; -{% endif %} -{% if subnet_config.smtp_server is vyos_defined %} - option smtp-server {{ subnet_config.smtp_server | join(', ') }}; -{% endif %} -{% if subnet_config.time_server is vyos_defined %} - option time-servers {{ subnet_config.time_server | join(', ') }}; -{% endif %} -{% if subnet_config.wins_server is vyos_defined %} - option netbios-name-servers {{ subnet_config.wins_server | join(', ') }}; -{% endif %} -{% if subnet_config.ipv6_only_preferred is vyos_defined %} - option rfc8925-ipv6-only-preferred {{ subnet_config.ipv6_only_preferred }}; -{% endif %} -{% if subnet_config.static_route is vyos_defined %} -{% set static_default_route = '' %} -{% if subnet_config.default_router is vyos_defined %} -{% set static_default_route = ', ' ~ '0.0.0.0/0' | isc_static_route(subnet_config.default_router) %} -{% endif %} -{% if subnet_config.static_route is vyos_defined %} -{% set rfc3442_routes = [] %} -{% for route, route_options in subnet_config.static_route.items() %} -{% set rfc3442_routes = rfc3442_routes.append(route | isc_static_route(route_options.next_hop)) %} -{% endfor %} - option rfc3442-static-route {{ rfc3442_routes | join(', ') }}{{ static_default_route }}; - option windows-static-route {{ rfc3442_routes | join(', ') }}; -{% endif %} -{% endif %} -{% if subnet_config.ip_forwarding is vyos_defined %} - option ip-forwarding true; -{% endif %} -{% if subnet_config.default_router is vyos_defined %} - option routers {{ subnet_config.default_router }}; -{% endif %} -{% if subnet_config.server_identifier is vyos_defined %} - option dhcp-server-identifier {{ subnet_config.server_identifier }}; -{% endif %} -{% if subnet_config.subnet_parameters is vyos_defined %} - # The following {{ subnet_config.subnet_parameters | length }} line(s) were added as - # subnet-parameters in the CLI and have not been validated!!! -{% for parameter in subnet_config.subnet_parameters %} - {{ parameter }} -{% endfor %} -{% endif %} -{% if subnet_config.tftp_server_name is vyos_defined %} - option tftp-server-name "{{ subnet_config.tftp_server_name }}"; -{% endif %} -{% if subnet_config.bootfile_name is vyos_defined %} - option bootfile-name "{{ subnet_config.bootfile_name }}"; - filename "{{ subnet_config.bootfile_name }}"; -{% endif %} -{% if subnet_config.bootfile_server is vyos_defined %} - next-server {{ subnet_config.bootfile_server }}; -{% endif %} -{% if subnet_config.bootfile_size is vyos_defined %} - option boot-size {{ subnet_config.bootfile_size }}; -{% endif %} -{% if subnet_config.time_offset is vyos_defined %} - option time-offset {{ subnet_config.time_offset }}; -{% endif %} -{% if subnet_config.wpad_url is vyos_defined %} - option wpad-url "{{ subnet_config.wpad_url }}"; -{% endif %} -{% if subnet_config.client_prefix_length is vyos_defined %} - option subnet-mask {{ ('0.0.0.0/' ~ subnet_config.client_prefix_length) | netmask_from_cidr }}; -{% endif %} -{% if subnet_config.lease is vyos_defined %} - default-lease-time {{ subnet_config.lease }}; - max-lease-time {{ subnet_config.lease }}; -{% endif %} -{% if network_config.ping_check is not vyos_defined and subnet_config.ping_check is vyos_defined %} - ping-check true; -{% endif %} -{% if subnet_config.static_mapping is vyos_defined %} -{% for host, host_config in subnet_config.static_mapping.items() if host_config.disable is not vyos_defined %} - host {{ host | replace('_','-') if host_decl_name is vyos_defined else network | replace('_','-') ~ '_' ~ host | replace('_','-') }} { -{% if host_config.ip_address is vyos_defined %} - fixed-address {{ host_config.ip_address }}; -{% endif %} - hardware ethernet {{ host_config.mac_address }}; -{% if host_config.static_mapping_parameters is vyos_defined %} - # The following {{ host_config.static_mapping_parameters | length }} line(s) were added - # as static-mapping-parameters in the CLI and have not been validated -{% for parameter in host_config.static_mapping_parameters %} - {{ parameter }} -{% endfor %} -{% endif %} - } -{% endfor %} -{% endif %} -{% if subnet_config.vendor_option.ubiquiti.unifi_controller is vyos_defined %} - option ubnt.unifi-controller {{ subnet_config.vendor_option.ubiquiti.unifi_controller }}; -{% endif %} -{% if subnet_config.range is vyos_defined %} -{# pool configuration can only be used if there follows a range option #} - pool { -{% endif %} -{% if subnet_config.enable_failover is vyos_defined %} - failover peer "{{ failover.name }}"; - deny dynamic bootp clients; -{% endif %} -{% if subnet_config.range is vyos_defined %} -{% for range, range_options in subnet_config.range.items() %} - range {{ range_options.start }} {{ range_options.stop }}; -{% endfor %} -{% endif %} -{% if subnet_config.range is vyos_defined %} -{# pool configuration can only be used if there follows a range option #} - } -{% endif %} - } -{% endfor %} -{% endif %} - on commit { - set shared-networkname = "{{ network }}"; -{% if hostfile_update is vyos_defined %} - set ClientIp = binary-to-ascii(10, 8, ".", leased-address); - set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name, "empty_hostname"); - if not (ClientName = "empty_hostname") { - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); - } else { - log(concat("Hostname is not defined for client with IP: ", ClientIP, " MAC: ", ClientMac)); - } -{% endif %} - } -} - -{% endfor %} -{% endif %} diff --git a/data/templates/dhcp-server/dhcpdv6.conf.j2 b/data/templates/dhcp-server/dhcpdv6.conf.j2 deleted file mode 100644 index 5c3471316..000000000 --- a/data/templates/dhcp-server/dhcpdv6.conf.j2 +++ /dev/null @@ -1,132 +0,0 @@ -### Autogenerated by dhcpv6_server.py ### - -# For options please consult the following website: -# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html - -log-facility local7; -{% if preference is vyos_defined %} -option dhcp6.preference {{ preference }}; -{% endif %} - -{% if global_parameters.name_server is vyos_defined %} -option dhcp6.name-servers {{ global_parameters.name_server | join(', ') }}; -{% endif %} - -# Vendor specific options - Cisco -option space cisco code width 2 length width 2; -option cisco.tftp-servers code 1 = array of ip6-address; -option vsio.cisco code 9 = encapsulate cisco; - -# Shared network configration(s) -{% if shared_network_name is vyos_defined %} -{% for network, network_config in shared_network_name.items() if network_config.disable is not vyos_defined %} -shared-network {{ network }} { -{% if network_config.common_options is vyos_defined %} -{% if network_config.common_options.info_refresh_time is vyos_defined %} - option dhcp6.info-refresh-time {{ network_config.common_options.info_refresh_time }}; -{% endif %} -{% if network_config.common_options.domain_search is vyos_defined %} - option dhcp6.domain-search "{{ network_config.common_options.domain_search | join('", "') }}"; -{% endif %} -{% if network_config.common_options.name_server is vyos_defined %} - option dhcp6.name-servers {{ network_config.common_options.name_server | join(', ') }}; -{% endif %} -{% endif %} -{% if network_config.subnet is vyos_defined %} -{% for subnet, subnet_config in network_config.subnet.items() %} - subnet6 {{ subnet }} { -{% if subnet_config.address_range is vyos_defined %} -{% if subnet_config.address_range.prefix is vyos_defined %} -{% for prefix, prefix_config in subnet_config.address_range.prefix.items() %} - range6 {{ prefix }} {{ "temporary" if prefix_config.temporary is vyos_defined }}; -{% endfor %} -{% endif %} -{% if subnet_config.address_range.start is vyos_defined %} -{% for address, address_config in subnet_config.address_range.start.items() %} - range6 {{ address }} {{ address_config.stop }}; -{% endfor %} -{% endif %} -{% endif %} -{% if subnet_config.domain_search is vyos_defined %} - option dhcp6.domain-search "{{ subnet_config.domain_search | join('", "') }}"; -{% endif %} -{% if subnet_config.lease_time is vyos_defined %} -{% if subnet_config.lease_time.default is vyos_defined %} - default-lease-time {{ subnet_config.lease_time.default }}; -{% endif %} -{% if subnet_config.lease_time.maximum is vyos_defined %} - max-lease-time {{ subnet_config.lease_time.maximum }}; -{% endif %} -{% if subnet_config.lease_time.minimum is vyos_defined %} - min-lease-time {{ subnet_config.lease_time.minimum }}; -{% endif %} -{% endif %} -{% if subnet_config.name_server is vyos_defined %} - option dhcp6.name-servers {{ subnet_config.name_server | join(', ') }}; -{% endif %} -{% if subnet_config.nis_domain is vyos_defined %} - option dhcp6.nis-domain-name "{{ subnet_config.nis_domain }}"; -{% endif %} -{% if subnet_config.nis_server is vyos_defined %} - option dhcp6.nis-servers {{ subnet_config.nis_server | join(', ') }}; -{% endif %} -{% if subnet_config.nisplus_domain is vyos_defined %} - option dhcp6.nisp-domain-name "{{ subnet_config.nisplus_domain }}"; -{% endif %} -{% if subnet_config.nisplus_server is vyos_defined %} - option dhcp6.nisp-servers {{ subnet_config.nisplus_server | join(', ') }}; -{% endif %} -{% if subnet_config.sip_server is vyos_defined %} -{% set server_ip = [] %} -{% set server_fqdn = [] %} -{% for address in subnet_config.sip_server %} -{% if address | is_ipv6 %} -{% set server_ip = server_ip.append(address) %} -{% else %} -{% set server_fqdn = server_fqdn.append(address) %} -{% endif %} -{% endfor %} -{% if server_ip is vyos_defined and server_ip | length > 0 %} - option dhcp6.sip-servers-addresses {{ server_ip | join(', ') }}; -{% endif %} -{% if server_fqdn is vyos_defined and server_fqdn | length > 0 %} - option dhcp6.sip-servers-names "{{ server_fqdn | join('", "') }}"; -{% endif %} -{% endif %} -{% if subnet_config.sntp_server is vyos_defined %} - option dhcp6.sntp-servers {{ subnet_config.sntp_server | join(', ') }}; -{% endif %} -{% if subnet_config.prefix_delegation.start is vyos_defined %} -{% for prefix, prefix_config in subnet_config.prefix_delegation.start.items() %} - prefix6 {{ prefix }} {{ prefix_config.stop }} /{{ prefix_config.prefix_length }}; -{% endfor %} -{% endif %} -{% if subnet_config.static_mapping is vyos_defined %} - - # begin configuration of static client mappings -{% for host, host_config in subnet_config.static_mapping.items() if host_config.disable is not vyos_defined %} - host {{ network | replace('_','-') }}_{{ host | replace('_','-') }} { -{% if host_config.identifier is vyos_defined %} - host-identifier option dhcp6.client-id {{ host_config.identifier }}; -{% endif %} -{% if host_config.ipv6_address is vyos_defined %} - fixed-address6 {{ host_config.ipv6_address }}; -{% endif %} -{% if host_config.ipv6_prefix is vyos_defined %} - fixed-prefix6 {{ host_config.ipv6_prefix }}; -{% endif %} - } -{% endfor %} -{% endif %} -{% if subnet_config.vendor_option.cisco.tftp_server is vyos_defined %} - option cisco.tftp-servers {{ subnet_config.vendor_option.cisco.tftp_server | join(', ') }}; -{% endif %} - } -{% endfor %} -{% endif %} - on commit { - set shared-networkname = "{{ network }}"; - } -} -{% endfor %} -{% endif %} diff --git a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 new file mode 100644 index 000000000..74c63a7a0 --- /dev/null +++ b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 @@ -0,0 +1,14 @@ +{ + "Control-agent": { +{% if failover is vyos_defined %} + "http-host": "{{ failover.source_address }}", + "http-port": 647, + "control-sockets": { + "dhcp4": { + "socket-type": "unix", + "socket-name": "/run/kea/dhcp4-ctrl-socket" + } + } +{% endif %} + } +} diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2 new file mode 100644 index 000000000..6ab13ab27 --- /dev/null +++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2 @@ -0,0 +1,72 @@ +{ + "Dhcp4": { + "interfaces-config": { + "interfaces": [ "*" ], + "dhcp-socket-type": "raw", + "service-sockets-max-retries": 5, + "service-sockets-retry-wait-time": 5000 + }, + "control-socket": { + "socket-type": "unix", + "socket-name": "/run/kea/dhcp4-ctrl-socket" + }, + "lease-database": { + "type": "memfile", + "persist": true, + "name": "{{ lease_file }}" + }, + "option-def": [ + { + "name": "rfc3442-static-route", + "code": 121, + "type": "record", + "array": true, + "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8,uint8" + }, + { + "name": "windows-static-route", + "code": 249, + "type": "record", + "array": true, + "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8,uint8" + }, + { + "name": "wpad-url", + "code": 252, + "type": "string" + }, + { + "name": "unifi-controller", + "code": 1, + "type": "ipv4-address", + "space": "ubnt" + } + ], + "hooks-libraries": [ +{% if failover is vyos_defined %} + { + "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_ha.so", + "parameters": { + "high-availability": [{{ failover | kea_failover_json }}] + } + }, +{% endif %} +{% if hostfile_update is vyos_defined %} + { + "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_run_script.so", + "parameters": { + "name": "/usr/libexec/vyos/system/on-dhcp-event.sh", + "sync": false + } + }, +{% endif %} + { + "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_lease_cmds.so", + "parameters": {} + } + ], +{% if shared_network_name is vyos_defined %} + "shared-networks": {{ shared_network_name | kea_shared_network_json }} +{% endif %} + } +} diff --git a/data/templates/dhcp-server/kea-dhcp6.conf.j2 b/data/templates/dhcp-server/kea-dhcp6.conf.j2 new file mode 100644 index 000000000..3ce4e6370 --- /dev/null +++ b/data/templates/dhcp-server/kea-dhcp6.conf.j2 @@ -0,0 +1,48 @@ +{ + "Dhcp6": { + "interfaces-config": { + "interfaces": [ "*" ], + "service-sockets-max-retries": 5, + "service-sockets-retry-wait-time": 5000 + }, + "control-socket": { + "socket-type": "unix", + "socket-name": "/run/kea/dhcp6-ctrl-socket" + }, + "lease-database": { + "type": "memfile", + "persist": true, + "name": "{{ lease_file }}" + }, + "hooks-libraries": [ + { + "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_lease_cmds.so", + "parameters": {} + } + ], + "option-data": [ +{% if global_parameters.name_server is vyos_defined %} + { + "name": "dns-servers", + "code": 23, + "space": "dhcp6", + "csv-format": true, + "data": "{{ global_parameters.name_server | join(", ") }}" + }{{ ',' if preference is vyos_defined else '' }} +{% endif %} +{% if preference is vyos_defined %} + { + "name": "preference", + "code": 7, + "space": "dhcp6", + "csv-format": true, + "data": "{{ preference }}" + } +{% endif %} + ], +{% if shared_network_name is vyos_defined %} + "shared-networks": {{ shared_network_name | kea6_shared_network_json }} +{% endif %} + + } +} diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index 6f81174ac..641dac44a 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -588,6 +588,14 @@ bgp route-reflector allow-outbound-policy {% if parameters.tcp_keepalive.idle is vyos_defined and parameters.tcp_keepalive.interval is vyos_defined and parameters.tcp_keepalive.probes is vyos_defined %} bgp tcp-keepalive {{ parameters.tcp_keepalive.idle }} {{ parameters.tcp_keepalive.interval }} {{ parameters.tcp_keepalive.probes }} {% endif %} +{% if srv6.locator is vyos_defined %} + segment-routing srv6 + locator {{ srv6.locator }} + exit +{% endif %} +{% if sid.vpn.per_vrf.export is vyos_defined %} + sid vpn per-vrf export {{ sid.vpn.per_vrf.export }} +{% endif %} {% if timers.keepalive is vyos_defined and timers.holdtime is vyos_defined %} timers bgp {{ timers.keepalive }} {{ timers.holdtime }} {% endif %} diff --git a/data/templates/frr/eigrpd.frr.j2 b/data/templates/frr/eigrpd.frr.j2 index 67f8a3ad1..3038a0b1d 100644 --- a/data/templates/frr/eigrpd.frr.j2 +++ b/data/templates/frr/eigrpd.frr.j2 @@ -1,21 +1,21 @@ -!
-router eigrp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }}
-{% if maximum_paths is vyos_defined %}
-maximum-paths {{ maximum_paths }}
-{% endif %}
-{% if metric.weights is vyos_defined %}
-metric weights {{ metric.weights }}
-{% endif %}
-{% if network is vyos_defined %}
-{% for net in network %}
-network {{ net }}
-{% endfor %}
-{% endif %}
-{% if redistribute is vyos_defined %}
-{% for protocol in redistribute %}
-redistribute {{ protocol }}
-{% endfor %}
-{% endif %}
-{% if variance is vyos_defined %}
-variance {{ variance }}
+! +router eigrp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} +{% if maximum_paths is vyos_defined %} +maximum-paths {{ maximum_paths }} +{% endif %} +{% if metric.weights is vyos_defined %} +metric weights {{ metric.weights }} +{% endif %} +{% if network is vyos_defined %} +{% for net in network %} +network {{ net }} +{% endfor %} +{% endif %} +{% if redistribute is vyos_defined %} +{% for protocol in redistribute %} +redistribute {{ protocol }} +{% endfor %} +{% endif %} +{% if variance is vyos_defined %} +variance {{ variance }} {% endif %}
\ No newline at end of file diff --git a/data/templates/frr/zebra.segment_routing.frr.j2 b/data/templates/frr/zebra.segment_routing.frr.j2 new file mode 100644 index 000000000..7b12fcdd0 --- /dev/null +++ b/data/templates/frr/zebra.segment_routing.frr.j2 @@ -0,0 +1,23 @@ +! +{% if srv6.locator is vyos_defined %} +segment-routing + srv6 + locators +{% for locator, locator_config in srv6.locator.items() %} + locator {{ locator }} +{% if locator_config.prefix is vyos_defined %} + prefix {{ locator_config.prefix }} block-len {{ locator_config.block_len }} node-len {{ locator_config.node_len }} func-bits {{ locator_config.func_bits }} +{% endif %} +{% if locator_config.behavior_usid is vyos_defined %} + behavior usid +{% endif %} + exit + ! +{% endfor %} + exit + ! +exit +! +exit +! +{% endif %} diff --git a/data/templates/system/sysctl.conf.j2 b/data/templates/system/sysctl.conf.j2 index 59a19e157..db699c3d8 100644 --- a/data/templates/system/sysctl.conf.j2 +++ b/data/templates/system/sysctl.conf.j2 @@ -1,7 +1,7 @@ -# autogenerated by system_sysctl.py
-
-{% if parameter is vyos_defined %}
-{% for k, v in parameter.items() %}
-{{ k }} = {{ v.value }}
-{% endfor %}
-{% endif %}
+# autogenerated by system_sysctl.py + +{% if parameter is vyos_defined %} +{% for k, v in parameter.items() %} +{{ k }} = {{ v.value }} +{% endfor %} +{% endif %} diff --git a/data/templates/telegraf/telegraf.j2 b/data/templates/telegraf/telegraf.j2 index 02a9656da..9623bdec6 100644 --- a/data/templates/telegraf/telegraf.j2 +++ b/data/templates/telegraf/telegraf.j2 @@ -89,6 +89,8 @@ ignore_fs = ["devtmpfs", "devfs"] [[inputs.diskio]] [[inputs.mem]] +[[inputs.net]] + ignore_protocol_stats = true [[inputs.nstat]] [[inputs.system]] [[inputs.netstat]] diff --git a/debian/control b/debian/control index f20268444..08adc8a68 100644 --- a/debian/control +++ b/debian/control @@ -169,8 +169,7 @@ Depends: # For "service dhcp-relay" isc-dhcp-relay, # For "service dhcp-server" - isc-dhcp-server, - python3-isc-dhcp-leases, + kea, # End "service dhcp-server" # For "service lldp" lldpd, @@ -280,6 +279,8 @@ Depends: # For "run monitor traffic" tcpdump, # End "run monitor traffic" +# For "show hardware dmi" + dmidecode, # For "run show hardware storage smart" smartmontools, # For "run show hardware scsi" diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index 64c60a780..cd88cf60c 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -21,14 +21,6 @@ if ! grep -q '^openvpn' /etc/passwd; then adduser --quiet --firstuid 100 --system --group --shell /usr/sbin/nologin openvpn fi -# Enable 2FA/MFA support for SSH and local logins -for file in /etc/pam.d/sshd /etc/pam.d/login -do - PAM_CONFIG="# Check 2FA/MFA authentication token if enabled (per user)\nauth required pam_google_authenticator.so nullok forward_pass\n" - grep -qF -- "pam_google_authenticator.so" $file || \ - sed -i "/^# Standard Un\*x authentication\./i${PAM_CONFIG}" $file -done - # We need to have a group for RADIUS service users to use it inside PAM rules if ! grep -q '^radius' /etc/group; then addgroup --firstgid 1000 --quiet radius diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 583de7ba9..081f7ed42 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -63,26 +63,16 @@ <constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage> </properties> </leafNode> + #include <include/pki/ca-certificate.xml.i> + #include <include/pki/certificate.xml.i> </children> </node> - <leafNode name="global-parameters"> - <properties> - <help>Additional global parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> - <multi/> - </properties> - </leafNode> <leafNode name="hostfile-update"> <properties> <help>Updating /etc/hosts file (per client lease)</help> <valueless/> </properties> </leafNode> - <leafNode name="host-decl-name"> - <properties> - <help>Use host declaration name for forward DNS name</help> - <valueless/> - </properties> - </leafNode> #include <include/listen-address-ipv4.xml.i> <tagNode name="shared-network-name"> <properties> @@ -102,16 +92,9 @@ #include <include/dhcp/domain-name.xml.i> #include <include/dhcp/domain-search.xml.i> #include <include/dhcp/ntp-server.xml.i> - #include <include/dhcp/ping-check.xml.i> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> #include <include/name-server-ipv4.xml.i> - <leafNode name="shared-network-parameters"> - <properties> - <help>Additional shared-network parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> - <multi/> - </properties> - </leafNode> <tagNode name="subnet"> <properties> <help>DHCP subnet for shared network</help> @@ -162,6 +145,7 @@ </constraint> </properties> </leafNode> + #include <include/dhcp/captive-portal.xml.i> <leafNode name="client-prefix-length"> <properties> <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help> @@ -191,12 +175,6 @@ #include <include/dhcp/domain-search.xml.i> #include <include/generic-description.xml.i> #include <include/name-server-ipv4.xml.i> - <leafNode name="enable-failover"> - <properties> - <help>Enable DHCP failover support for this subnet</help> - <valueless/> - </properties> - </leafNode> <leafNode name="exclude"> <properties> <help>IP address to exclude from DHCP lease range</help> @@ -231,7 +209,6 @@ <defaultValue>86400</defaultValue> </leafNode> #include <include/dhcp/ntp-server.xml.i> - #include <include/dhcp/ping-check.xml.i> <leafNode name="pop-server"> <properties> <help>IP address of POP3 server</help> @@ -339,12 +316,6 @@ </constraint> </properties> </leafNode> - <leafNode name="static-mapping-parameters"> - <properties> - <help>Additional static-mapping parameters for DHCP server. Will be placed inside the "host" block of the mapping. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> - <multi/> - </properties> - </leafNode> </children> </tagNode> <tagNode name="static-route"> @@ -386,12 +357,6 @@ <constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage> </properties> </leafNode> - <leafNode name="subnet-parameters"> - <properties> - <help>Additional subnet parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> - <multi/> - </properties> - </leafNode> <leafNode name="tftp-server-name"> <properties> <help>TFTP server name</help> @@ -435,6 +400,17 @@ <multi/> </properties> </leafNode> + <leafNode name="time-zone"> + <properties> + <help>Time zone to send to clients. Uses RFC4833 options 100 and 101</help> + <completionHelp> + <script>timedatectl list-timezones</script> + </completionHelp> + <constraint> + <validator name="timezone" argument="--validate"/> + </constraint> + </properties> + </leafNode> <node name="vendor-option"> <properties> <help>Vendor Specific Options</help> diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in index 9dff68a24..b37f79434 100644 --- a/interface-definitions/dhcpv6-server.xml.in +++ b/interface-definitions/dhcpv6-server.xml.in @@ -41,6 +41,21 @@ <children> #include <include/generic-disable-node.xml.i> #include <include/generic-description.xml.i> + <leafNode name="interface"> + <properties> + <help>Optional interface for this shared network to accept requests from</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> + </properties> + </leafNode> <node name="common-options"> <properties> <help>Common options to distribute to all clients, including stateless clients</help> @@ -79,7 +94,7 @@ <help>Parameters setting ranges for assigning IPv6 addresses</help> </properties> <children> - <tagNode name="prefix"> + <leafNode name="prefix"> <properties> <help>IPv6 prefix defining range of addresses to assign</help> <valueHelp> @@ -89,16 +104,9 @@ <constraint> <validator name="ipv6-prefix"/> </constraint> + <multi/> </properties> - <children> - <leafNode name="temporary"> - <properties> - <help>Address range will be used for temporary addresses</help> - <valueless/> - </properties> - </leafNode> - </children> - </tagNode> + </leafNode> <tagNode name="start"> <properties> <help>First in range of consecutive IPv6 addresses to assign</help> @@ -127,6 +135,7 @@ </tagNode> </children> </node> + #include <include/dhcp/captive-portal.xml.i> #include <include/dhcp/domain-search.xml.i> <node name="lease-time"> <properties> @@ -221,12 +230,12 @@ <help>Parameters relating to IPv6 prefix delegation</help> </properties> <children> - <tagNode name="start"> + <tagNode name="prefix"> <properties> - <help>First in range of IPv6 addresses to be used in prefix delegation</help> + <help>IPv6 prefix to be used in prefix delegation</help> <valueHelp> <format>ipv6</format> - <description>IPv6 address used in prefix delegation</description> + <description>IPv6 prefix used in prefix delegation</description> </valueHelp> <constraint> <validator name="ipv6-address"/> @@ -235,27 +244,28 @@ <children> <leafNode name="prefix-length"> <properties> - <help>Length in bits of prefixes to be delegated</help> + <help>Length in bits of prefix</help> <valueHelp> <format>u32:32-64</format> - <description>Delagated prefix length (32-64)</description> + <description>Prefix length (32-64)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 32-64"/> </constraint> - <constraintErrorMessage>Delegated prefix length must be between 32 and 64</constraintErrorMessage> + <constraintErrorMessage>Prefix length must be between 32 and 64</constraintErrorMessage> </properties> </leafNode> - <leafNode name="stop"> + <leafNode name="delegated-length"> <properties> - <help>Last in range of IPv6 addresses to be used in prefix delegation</help> + <help>Length in bits of prefixes to be delegated</help> <valueHelp> - <format>ipv6</format> - <description>IPv6 address used in prefix delegation</description> + <format>u32:32-64</format> + <description>Delegated prefix length (32-64)</description> </valueHelp> <constraint> - <validator name="ipv6-address"/> + <validator name="numeric" argument="--range 32-96"/> </constraint> + <constraintErrorMessage>Delegated prefix length must be between 32 and 96</constraintErrorMessage> </properties> </leafNode> </children> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index 4e43298bc..c379ba95c 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -1639,6 +1639,66 @@ #include <include/port-number.xml.i> </children> </tagNode> +<node name="srv6"> + <properties> + <help>Segment-Routing SRv6 configuration</help> + </properties> + <children> + <leafNode name="locator"> + <properties> + <help>Specify SRv6 locator</help> + <valueHelp> + <format>txt</format> + <description>SRv6 locator name</description> + </valueHelp> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="sid"> + <properties> + <help>SID value for VRF</help> + </properties> + <children> + <node name="vpn"> + <properties> + <help>Between current VRF and VPN</help> + </properties> + <children> + <node name="per-vrf"> + <properties> + <help>SID per-VRF (both IPv4 and IPv6 address families)</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>For routes leaked from current VRF to VPN</help> + <completionHelp> + <list>auto</list> + </completionHelp> + <valueHelp> + <format>u32:1-1048575</format> + <description>SID allocation index</description> + </valueHelp> + <valueHelp> + <format>auto</format> + <description>Automatically assign a label</description> + </valueHelp> + <constraint> + <regex>auto</regex> + <validator name="numeric" argument="--range 1-1048575"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> +</node> <node name="timers"> <properties> <help>BGP protocol timers</help> diff --git a/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore.xml.i b/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore.xml.i index ba097c6b5..399f2e1da 100644 --- a/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore.xml.i +++ b/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore.xml.i @@ -1,3 +1,3 @@ -<!-- include start from include/constraint/alpha-numeric-hyphen-underscore.xml.i --> +<!-- include start from constraint/alpha-numeric-hyphen-underscore.xml.i --> <regex>[-_a-zA-Z0-9]+</regex> <!-- include end --> diff --git a/interface-definitions/include/constraint/dhcp-client-string-option.xml.i b/interface-definitions/include/constraint/dhcp-client-string-option.xml.i index 76e0e5466..88257a9bb 100644 --- a/interface-definitions/include/constraint/dhcp-client-string-option.xml.i +++ b/interface-definitions/include/constraint/dhcp-client-string-option.xml.i @@ -1,4 +1,4 @@ -<!-- include start from include/constraint/dhcp-client-string-option.xml.i --> +<!-- include start from constraint/dhcp-client-string-option.xml.i --> <regex>[-_a-zA-Z0-9\s]+</regex> <regex>([a-fA-F0-9][a-fA-F0-9]:){2,}[a-fA-F0-9][a-fA-F0-9]</regex> <!-- include end --> diff --git a/interface-definitions/include/constraint/host-name.xml.i b/interface-definitions/include/constraint/host-name.xml.i index cc9740c16..5943772a2 100644 --- a/interface-definitions/include/constraint/host-name.xml.i +++ b/interface-definitions/include/constraint/host-name.xml.i @@ -1,3 +1,3 @@ -<!-- include start from constraint/host-name.xml.i -->
-<regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex>
-<!-- include end -->
+<!-- include start from constraint/host-name.xml.i --> +<regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex> +<!-- include end --> diff --git a/interface-definitions/include/dhcp/captive-portal.xml.i b/interface-definitions/include/dhcp/captive-portal.xml.i new file mode 100644 index 000000000..643f055a8 --- /dev/null +++ b/interface-definitions/include/dhcp/captive-portal.xml.i @@ -0,0 +1,11 @@ +<!-- include start from dhcp/captive-portal.xml.i --> +<leafNode name="captive-portal"> + <properties> + <help>Captive portal API endpoint</help> + <valueHelp> + <format>txt</format> + <description>Captive portal API endpoint</description> + </valueHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/dhcp/ping-check.xml.i b/interface-definitions/include/dhcp/ping-check.xml.i deleted file mode 100644 index a506f68e4..000000000 --- a/interface-definitions/include/dhcp/ping-check.xml.i +++ /dev/null @@ -1,8 +0,0 @@ -<!-- include start from dhcp/ping-check.xml.i --> -<leafNode name="ping-check"> - <properties> - <help>Sends ICMP Echo request to the address being assigned</help> - <valueless/> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/policy/route-common.xml.i b/interface-definitions/include/policy/route-common.xml.i index b8581b03e..e412fe58e 100644 --- a/interface-definitions/include/policy/route-common.xml.i +++ b/interface-definitions/include/policy/route-common.xml.i @@ -1,257 +1,257 @@ -<!-- include start from policy/route-common.xml.i -->
-#include <include/policy/route-rule-action.xml.i>
-#include <include/generic-description.xml.i>
-#include <include/firewall/firewall-mark.xml.i>
-#include <include/generic-disable-node.xml.i>
-<node name="fragment">
- <properties>
- <help>IP fragment match</help>
- </properties>
- <children>
- <leafNode name="match-frag">
- <properties>
- <help>Second and further fragments of fragmented packets</help>
- <valueless/>
- </properties>
- </leafNode>
- <leafNode name="match-non-frag">
- <properties>
- <help>Head fragments or unfragmented packets</help>
- <valueless/>
- </properties>
- </leafNode>
- </children>
-</node>
-<node name="ipsec">
- <properties>
- <help>Inbound IPsec packets</help>
- </properties>
- <children>
- <leafNode name="match-ipsec">
- <properties>
- <help>Inbound IPsec packets</help>
- <valueless/>
- </properties>
- </leafNode>
- <leafNode name="match-none">
- <properties>
- <help>Inbound non-IPsec packets</help>
- <valueless/>
- </properties>
- </leafNode>
- </children>
-</node>
-<node name="limit">
- <properties>
- <help>Rate limit using a token bucket filter</help>
- </properties>
- <children>
- <leafNode name="burst">
- <properties>
- <help>Maximum number of packets to allow in excess of rate</help>
- <valueHelp>
- <format>u32:0-4294967295</format>
- <description>Maximum number of packets to allow in excess of rate</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-4294967295"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="rate">
- <properties>
- <help>Maximum average matching rate</help>
- <valueHelp>
- <format>u32:0-4294967295</format>
- <description>Maximum average matching rate</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-4294967295"/>
- </constraint>
- </properties>
- </leafNode>
- </children>
-</node>
-#include <include/firewall/log.xml.i>
-<leafNode name="protocol">
- <properties>
- <help>Protocol to match (protocol name, number, or "all")</help>
- <completionHelp>
- <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script>
- </completionHelp>
- <valueHelp>
- <format>all</format>
- <description>All IP protocols</description>
- </valueHelp>
- <valueHelp>
- <format>tcp_udp</format>
- <description>Both TCP and UDP</description>
- </valueHelp>
- <valueHelp>
- <format>0-255</format>
- <description>IP protocol number</description>
- </valueHelp>
- <valueHelp>
- <format>!<protocol></format>
- <description>IP protocol number</description>
- </valueHelp>
- <constraint>
- <validator name="ip-protocol"/>
- </constraint>
- </properties>
- <defaultValue>all</defaultValue>
-</leafNode>
-<node name="recent">
- <properties>
- <help>Parameters for matching recently seen sources</help>
- </properties>
- <children>
- <leafNode name="count">
- <properties>
- <help>Source addresses seen more than N times</help>
- <valueHelp>
- <format>u32:1-255</format>
- <description>Source addresses seen more than N times</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-255"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="time">
- <properties>
- <help>Source addresses seen in the last N seconds</help>
- <valueHelp>
- <format>u32:0-4294967295</format>
- <description>Source addresses seen in the last N seconds</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-4294967295"/>
- </constraint>
- </properties>
- </leafNode>
- </children>
-</node>
-<node name="set">
- <properties>
- <help>Packet modifications</help>
- </properties>
- <children>
- <leafNode name="connection-mark">
- <properties>
- <help>Connection marking</help>
- <valueHelp>
- <format>u32:0-2147483647</format>
- <description>Connection marking</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-2147483647"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="dscp">
- <properties>
- <help>Packet Differentiated Services Codepoint (DSCP)</help>
- <valueHelp>
- <format>u32:0-63</format>
- <description>DSCP number</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 0-63"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="mark">
- <properties>
- <help>Packet marking</help>
- <valueHelp>
- <format>u32:1-2147483647</format>
- <description>Packet marking</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-2147483647"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="table">
- <properties>
- <help>Routing table to forward packet with</help>
- <valueHelp>
- <format>u32:1-200</format>
- <description>Table number</description>
- </valueHelp>
- <valueHelp>
- <format>main</format>
- <description>Main table</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-200"/>
- <regex>(main)</regex>
- </constraint>
- <completionHelp>
- <list>main</list>
- <path>protocols static table</path>
- </completionHelp>
- </properties>
- </leafNode>
- <leafNode name="tcp-mss">
- <properties>
- <help>TCP Maximum Segment Size</help>
- <valueHelp>
- <format>u32:500-1460</format>
- <description>Explicitly set TCP MSS value</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 500-1460"/>
- </constraint>
- </properties>
- </leafNode>
- </children>
-</node>
-#include <include/firewall/state.xml.i>
-#include <include/firewall/tcp-flags.xml.i>
-#include <include/firewall/tcp-mss.xml.i>
-<node name="time">
- <properties>
- <help>Time to match rule</help>
- </properties>
- <children>
- <leafNode name="monthdays">
- <properties>
- <help>Monthdays to match rule on</help>
- </properties>
- </leafNode>
- <leafNode name="startdate">
- <properties>
- <help>Date to start matching rule</help>
- </properties>
- </leafNode>
- <leafNode name="starttime">
- <properties>
- <help>Time of day to start matching rule</help>
- </properties>
- </leafNode>
- <leafNode name="stopdate">
- <properties>
- <help>Date to stop matching rule</help>
- </properties>
- </leafNode>
- <leafNode name="stoptime">
- <properties>
- <help>Time of day to stop matching rule</help>
- </properties>
- </leafNode>
- <leafNode name="utc">
- <properties>
- <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help>
- <valueless/>
- </properties>
- </leafNode>
- <leafNode name="weekdays">
- <properties>
- <help>Weekdays to match rule on</help>
- </properties>
- </leafNode>
- </children>
-</node>
-<!-- include end -->
+<!-- include start from policy/route-common.xml.i --> +#include <include/policy/route-rule-action.xml.i> +#include <include/generic-description.xml.i> +#include <include/firewall/firewall-mark.xml.i> +#include <include/generic-disable-node.xml.i> +<node name="fragment"> + <properties> + <help>IP fragment match</help> + </properties> + <children> + <leafNode name="match-frag"> + <properties> + <help>Second and further fragments of fragmented packets</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-non-frag"> + <properties> + <help>Head fragments or unfragmented packets</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<node name="ipsec"> + <properties> + <help>Inbound IPsec packets</help> + </properties> + <children> + <leafNode name="match-ipsec"> + <properties> + <help>Inbound IPsec packets</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-none"> + <properties> + <help>Inbound non-IPsec packets</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<node name="limit"> + <properties> + <help>Rate limit using a token bucket filter</help> + </properties> + <children> + <leafNode name="burst"> + <properties> + <help>Maximum number of packets to allow in excess of rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Maximum number of packets to allow in excess of rate</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="rate"> + <properties> + <help>Maximum average matching rate</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Maximum average matching rate</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +#include <include/firewall/log.xml.i> +<leafNode name="protocol"> + <properties> + <help>Protocol to match (protocol name, number, or "all")</help> + <completionHelp> + <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All IP protocols</description> + </valueHelp> + <valueHelp> + <format>tcp_udp</format> + <description>Both TCP and UDP</description> + </valueHelp> + <valueHelp> + <format>0-255</format> + <description>IP protocol number</description> + </valueHelp> + <valueHelp> + <format>!<protocol></format> + <description>IP protocol number</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + <defaultValue>all</defaultValue> +</leafNode> +<node name="recent"> + <properties> + <help>Parameters for matching recently seen sources</help> + </properties> + <children> + <leafNode name="count"> + <properties> + <help>Source addresses seen more than N times</help> + <valueHelp> + <format>u32:1-255</format> + <description>Source addresses seen more than N times</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="time"> + <properties> + <help>Source addresses seen in the last N seconds</help> + <valueHelp> + <format>u32:0-4294967295</format> + <description>Source addresses seen in the last N seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="set"> + <properties> + <help>Packet modifications</help> + </properties> + <children> + <leafNode name="connection-mark"> + <properties> + <help>Connection marking</help> + <valueHelp> + <format>u32:0-2147483647</format> + <description>Connection marking</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dscp"> + <properties> + <help>Packet Differentiated Services Codepoint (DSCP)</help> + <valueHelp> + <format>u32:0-63</format> + <description>DSCP number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-63"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mark"> + <properties> + <help>Packet marking</help> + <valueHelp> + <format>u32:1-2147483647</format> + <description>Packet marking</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + </properties> + </leafNode> + <leafNode name="table"> + <properties> + <help>Routing table to forward packet with</help> + <valueHelp> + <format>u32:1-200</format> + <description>Table number</description> + </valueHelp> + <valueHelp> + <format>main</format> + <description>Main table</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-200"/> + <regex>(main)</regex> + </constraint> + <completionHelp> + <list>main</list> + <path>protocols static table</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="tcp-mss"> + <properties> + <help>TCP Maximum Segment Size</help> + <valueHelp> + <format>u32:500-1460</format> + <description>Explicitly set TCP MSS value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 500-1460"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +#include <include/firewall/state.xml.i> +#include <include/firewall/tcp-flags.xml.i> +#include <include/firewall/tcp-mss.xml.i> +<node name="time"> + <properties> + <help>Time to match rule</help> + </properties> + <children> + <leafNode name="monthdays"> + <properties> + <help>Monthdays to match rule on</help> + </properties> + </leafNode> + <leafNode name="startdate"> + <properties> + <help>Date to start matching rule</help> + </properties> + </leafNode> + <leafNode name="starttime"> + <properties> + <help>Time of day to start matching rule</help> + </properties> + </leafNode> + <leafNode name="stopdate"> + <properties> + <help>Date to stop matching rule</help> + </properties> + </leafNode> + <leafNode name="stoptime"> + <properties> + <help>Time of day to stop matching rule</help> + </properties> + </leafNode> + <leafNode name="utc"> + <properties> + <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="weekdays"> + <properties> + <help>Weekdays to match rule on</help> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/protocol-tcp-udp.xml.i b/interface-definitions/include/protocol-tcp-udp.xml.i index d7e6752ad..c186c026a 100644 --- a/interface-definitions/include/protocol-tcp-udp.xml.i +++ b/interface-definitions/include/protocol-tcp-udp.xml.i @@ -1,22 +1,22 @@ -<!-- include start from snmp/protocol.xml.i -->
-<leafNode name="protocol">
- <properties>
- <help>Protocol to be used (TCP/UDP)</help>
- <completionHelp>
- <list>udp tcp</list>
- </completionHelp>
- <valueHelp>
- <format>udp</format>
- <description>Listen protocol UDP</description>
- </valueHelp>
- <valueHelp>
- <format>tcp</format>
- <description>Listen protocol TCP</description>
- </valueHelp>
- <constraint>
- <regex>(udp|tcp)</regex>
- </constraint>
- </properties>
- <defaultValue>udp</defaultValue>
-</leafNode>
-<!-- include end -->
+<!-- include start from snmp/protocol.xml.i --> +<leafNode name="protocol"> + <properties> + <help>Protocol to be used (TCP/UDP)</help> + <completionHelp> + <list>udp tcp</list> + </completionHelp> + <valueHelp> + <format>udp</format> + <description>Listen protocol UDP</description> + </valueHelp> + <valueHelp> + <format>tcp</format> + <description>Listen protocol TCP</description> + </valueHelp> + <constraint> + <regex>(udp|tcp)</regex> + </constraint> + </properties> + <defaultValue>udp</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/rip/version.xml.i b/interface-definitions/include/rip/version.xml.i index a35350aee..61458b28e 100644 --- a/interface-definitions/include/rip/version.xml.i +++ b/interface-definitions/include/rip/version.xml.i @@ -1,18 +1,18 @@ -<!-- include start from rip/version.xml.i -->
-<leafNode name="version">
- <properties>
- <help>Limit RIP protocol version</help>
- <valueHelp>
- <format>1</format>
- <description>Allow RIPv1 only</description>
- </valueHelp>
- <valueHelp>
- <format>2</format>
- <description>Allow RIPv2 only</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-2"/>
- </constraint>
- </properties>
-</leafNode>
-<!-- include end -->
+<!-- include start from rip/version.xml.i --> +<leafNode name="version"> + <properties> + <help>Limit RIP protocol version</help> + <valueHelp> + <format>1</format> + <description>Allow RIPv1 only</description> + </valueHelp> + <valueHelp> + <format>2</format> + <description>Allow RIPv2 only</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i index 330cb7d1b..7c4b5633e 100644 --- a/interface-definitions/include/version/dhcp-server-version.xml.i +++ b/interface-definitions/include/version/dhcp-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dhcp-server-version.xml.i --> -<syntaxVersion component='dhcp-server' version='6'></syntaxVersion> +<syntaxVersion component='dhcp-server' version='7'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i index 4b2cf40aa..ae4178c90 100644 --- a/interface-definitions/include/version/dhcpv6-server-version.xml.i +++ b/interface-definitions/include/version/dhcpv6-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dhcpv6-server-version.xml.i --> -<syntaxVersion component='dhcpv6-server' version='1'></syntaxVersion> +<syntaxVersion component='dhcpv6-server' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/protocols-segment-routing.xml.in b/interface-definitions/protocols-segment-routing.xml.in new file mode 100644 index 000000000..d461e9c5d --- /dev/null +++ b/interface-definitions/protocols-segment-routing.xml.in @@ -0,0 +1,89 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="segment-routing" owner="${vyos_conf_scripts_dir}/protocols_segment_routing.py"> + <properties> + <help>Segment Routing</help> + <priority>900</priority> + </properties> + <children> + <node name="srv6"> + <properties> + <help>Segment-Routing SRv6 configuration</help> + </properties> + <children> + <tagNode name="locator"> + <properties> + <help>Segment Routing SRv6 locator</help> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> + </constraint> + </properties> + <children> + <leafNode name="behavior-usid"> + <properties> + <help>Set SRv6 behavior uSID</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="prefix"> + <properties> + <help>SRv6 locator prefix</help> + <valueHelp> + <format>ipv6net</format> + <description>SRv6 locator prefix</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="block-len"> + <properties> + <help>Configure SRv6 locator block length in bits</help> + <valueHelp> + <format>u32:16-64</format> + <description>Specify SRv6 locator block length in bits</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 16-64"/> + </constraint> + </properties> + <defaultValue>40</defaultValue> + </leafNode> + <leafNode name="func-bits"> + <properties> + <help>Configure SRv6 locator function length in bits</help> + <valueHelp> + <format>u32:0-64</format> + <description>Specify SRv6 locator function length in bits</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-64"/> + </constraint> + </properties> + <defaultValue>16</defaultValue> + </leafNode> + <leafNode name="node-len"> + <properties> + <help>Configure SRv6 locator node length in bits</help> + <valueHelp> + <format>u32:16-64</format> + <description>Configure SRv6 locator node length in bits</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 16-64"/> + </constraint> + </properties> + <defaultValue>24</defaultValue> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in index 9c2e2be76..ceb321f3e 100644 --- a/op-mode-definitions/dhcp.xml.in +++ b/op-mode-definitions/dhcp.xml.in @@ -163,7 +163,7 @@ <properties> <help>Restart DHCP server</help> </properties> - <command>if cli-shell-api existsActive service dhcp-server; then sudo systemctl restart isc-dhcp-server.service; else echo "DHCP server not configured"; fi</command> + <command>if cli-shell-api existsActive service dhcp-server; then sudo systemctl restart kea-dhcp4-server.service; else echo "DHCP server not configured"; fi</command> </node> <node name="relay-agent"> <properties> @@ -182,7 +182,7 @@ <properties> <help>Restart DHCPv6 server</help> </properties> - <command>if cli-shell-api existsActive service dhcpv6-server; then sudo systemctl restart isc-dhcp-server6.service; else echo "DHCPv6 server not configured"; fi</command> + <command>if cli-shell-api existsActive service dhcpv6-server; then sudo systemctl restart kea-dhcp6-server.service; else echo "DHCPv6 server not configured"; fi</command> </node> <node name="relay-agent"> <properties> diff --git a/op-mode-definitions/generate-system-login-user.xml.in b/op-mode-definitions/generate-system-login-user.xml.in index 237a13610..bd80840df 100755 --- a/op-mode-definitions/generate-system-login-user.xml.in +++ b/op-mode-definitions/generate-system-login-user.xml.in @@ -1,90 +1,90 @@ -<?xml version="1.0"?>
-<interfaceDefinition>
- <node name="generate">
- <children>
- <node name="system">
- <properties>
- <help>Generate system related parameters</help>
- </properties>
- <children>
- <node name="login">
- <properties>
- <help>Generate system login related parameters</help>
- </properties>
- <children>
- <tagNode name="username">
- <properties>
- <help>Username used for authentication</help>
- <completionHelp>
- <list><username></list>
- </completionHelp>
- </properties>
- <children>
- <node name="otp-key">
- <properties>
- <help>Generate OpenConnect OTP token</help>
- </properties>
- <children>
- <node name="hotp-time">
- <properties>
- <help>HOTP time-based token</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5"</command>
- <children>
- <tagNode name="rate-limit">
- <properties>
- <help>Duration of single time interval</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "$9"</command>
- <children>
- <tagNode name="rate-time">
- <properties>
- <help>The number of digits in the one-time password</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "$9" --rate-time "${11}" </command>
- <children>
- <tagNode name="window-size">
- <properties>
- <help>The number of digits in the one-time password</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "$9" --rate-time "${11}" --window-size "${13}"</command>
- </tagNode>
- </children>
- </tagNode>
- </children>
- </tagNode>
- <tagNode name="window-size">
- <properties>
- <help>The number of digits in the one-time password</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --window-size "${9}"</command>
- <children>
- <tagNode name="rate-limit">
- <properties>
- <help>Duration of single time interval</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "${11}" --window-size "${9}"</command>
- <children>
- <tagNode name="rate-time">
- <properties>
- <help>Duration of single time interval</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "${11}" --rate-time "${13}" --window-size "${9}"</command>
- </tagNode>
- </children>
- </tagNode>
- </children>
- </tagNode>
- </children>
- </node>
- </children>
- </node>
- </children>
- </tagNode>
- </children>
- </node>
- </children>
- </node>
- </children>
- </node>
-</interfaceDefinition>
+<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="system"> + <properties> + <help>Generate system related parameters</help> + </properties> + <children> + <node name="login"> + <properties> + <help>Generate system login related parameters</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>Username used for authentication</help> + <completionHelp> + <path>system login user</path> + </completionHelp> + </properties> + <children> + <node name="otp-key"> + <properties> + <help>Generate OpenConnect OTP token</help> + </properties> + <children> + <node name="hotp-time"> + <properties> + <help>HOTP time-based token</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5"</command> + <children> + <tagNode name="rate-limit"> + <properties> + <help>Duration of single time interval</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "$9"</command> + <children> + <tagNode name="rate-time"> + <properties> + <help>The number of digits in the one-time password</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "$9" --rate-time "${11}" </command> + <children> + <tagNode name="window-size"> + <properties> + <help>The number of digits in the one-time password</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "$9" --rate-time "${11}" --window-size "${13}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="window-size"> + <properties> + <help>The number of digits in the one-time password</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --window-size "${9}"</command> + <children> + <tagNode name="rate-limit"> + <properties> + <help>Duration of single time interval</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "${11}" --window-size "${9}"</command> + <children> + <tagNode name="rate-time"> + <properties> + <help>Duration of single time interval</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate-limit "${11}" --rate-time "${13}" --window-size "${9}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/include/ospf/graceful-restart.xml.i b/op-mode-definitions/include/ospf/graceful-restart.xml.i index 736d8f951..848b19828 100644 --- a/op-mode-definitions/include/ospf/graceful-restart.xml.i +++ b/op-mode-definitions/include/ospf/graceful-restart.xml.i @@ -1,6 +1,6 @@ <node name="graceful-restart"> <properties> - <help>Show IPv4 OSPF Graceful Restart</help> + <help>Show OSPF Graceful Restart</help> </properties> <children> <leafNode name="helper"> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index 44628a112..3a8118dcb 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -51,7 +51,7 @@ <properties> <help>Monitor last lines of DHCP server log</help> </properties> - <command>journalctl --no-hostname --follow --boot --unit isc-dhcp-server.service</command> + <command>journalctl --no-hostname --follow --boot --unit kea-dhcp4-server.service</command> </node> <node name="client"> <properties> @@ -81,7 +81,7 @@ <properties> <help>Monitor last lines of DHCPv6 server log</help> </properties> - <command>journalctl --no-hostname --follow --boot --unit isc-dhcp-server6.service</command> + <command>journalctl --no-hostname --follow --boot --unit kea-dhcp6-server.service</command> </node> <node name="client"> <properties> diff --git a/op-mode-definitions/show-bgp.xml.in b/op-mode-definitions/show-bgp.xml.in index 3c212614c..8b1992432 100644 --- a/op-mode-definitions/show-bgp.xml.in +++ b/op-mode-definitions/show-bgp.xml.in @@ -100,6 +100,19 @@ </children> </tagNode> #include <include/vtysh-generic-wide.xml.i> + <node name="segment-routing"> + <properties> + <help>BGP Segment Routing</help> + </properties> + <children> + <leafNode name="srv6"> + <properties> + <help>BGP Segment Routing SRv6</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 3a622cfb5..399c6acf8 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -78,7 +78,7 @@ <properties> <help>Show log for DHCP server</help> </properties> - <command>journalctl --no-hostname --boot --unit isc-dhcp-server.service</command> + <command>journalctl --no-hostname --boot --unit kea-dhcp4-server.service</command> </node> <node name="client"> <properties> @@ -108,7 +108,7 @@ <properties> <help>Show log for DHCPv6 server</help> </properties> - <command>journalctl --no-hostname --boot --unit isc-dhcp-server6.service</command> + <command>journalctl --no-hostname --boot --unit kea-dhcp6-server.service</command> </node> <node name="client"> <properties> diff --git a/op-mode-definitions/show-segment-routing.xml.in b/op-mode-definitions/show-segment-routing.xml.in new file mode 100644 index 000000000..ebdb51a61 --- /dev/null +++ b/op-mode-definitions/show-segment-routing.xml.in @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="segment-routing"> + <properties> + <help>Show Segment Routing</help> + </properties> + <children> + <node name="srv6"> + <properties> + <help>Segment Routing SRv6</help> + </properties> + <children> + <node name="locator"> + <properties> + <help>Locator Information</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index df7240c88..e57129ce5 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -125,6 +125,7 @@ class ConfigMgmt: get_first_key=True) self.max_revisions = int(d.get('commit_revisions', 0)) + self.num_revisions = 0 self.locations = d.get('commit_archive', {}).get('location', []) self.source_address = d.get('commit_archive', {}).get('source_address', '') @@ -233,7 +234,7 @@ Proceed ?''' msg = '' if not self._check_revision_number(rev): - msg = f'Invalid revision number {rev}: must be 0 < rev < {maxrev}' + msg = f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}' return msg, 1 prompt_str = 'Proceed with reboot ?' @@ -385,15 +386,8 @@ Proceed ?''' _, _, netloc = url.netloc.rpartition("@") redacted_location = urlunsplit(url._replace(netloc=netloc)) print(f" {redacted_location}", end=" ", flush=True) - try: - upload(archive_config_file, f'{location}/{remote_file}', - source_host=source_address, raise_error=True) - print("OK") - except Exception as e: - print("FAILED!") - print() - print(indent(str(e), " > ")) - print() + upload(archive_config_file, f'{location}/{remote_file}', + source_host=source_address) # op-mode functions # @@ -560,8 +554,8 @@ Proceed ?''' return len(l) def _check_revision_number(self, rev: int) -> bool: - maxrev = self._get_number_of_revisions() - if not 0 <= rev < maxrev: + self.num_revisions = self._get_number_of_revisions() + if not 0 <= rev < self.num_revisions: return False return True diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 09cfd43d3..d048901f0 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -160,6 +160,9 @@ class ConfigTree(object): def _get_config(self): return self.__config + def get_version_string(self): + return self.__version + def to_string(self, ordered_values=False): config_string = self.__to_string(self.__config, ordered_values).decode() config_string = "{0}\n{1}".format(config_string, self.__version) diff --git a/python/vyos/kea.py b/python/vyos/kea.py new file mode 100644 index 000000000..cb341e0f2 --- /dev/null +++ b/python/vyos/kea.py @@ -0,0 +1,319 @@ +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import json +import os +import socket + +from datetime import datetime + +from vyos.template import is_ipv6 +from vyos.template import isc_static_route +from vyos.template import netmask_from_cidr +from vyos.utils.dict import dict_search_args +from vyos.utils.file import read_file + +kea4_options = { + 'name_server': 'domain-name-servers', + 'domain_name': 'domain-name', + 'domain_search': 'domain-search', + 'ntp_server': 'ntp-servers', + 'pop_server': 'pop-server', + 'smtp_server': 'smtp-server', + 'time_server': 'time-servers', + 'wins_server': 'netbios-name-servers', + 'default_router': 'routers', + 'server_identifier': 'dhcp-server-identifier', + 'tftp_server_name': 'tftp-server-name', + 'bootfile_size': 'boot-size', + 'time_offset': 'time-offset', + 'wpad_url': 'wpad-url', + 'ipv6_only_preferred': 'v6-only-preferred', + 'captive_portal': 'v4-captive-portal' +} + +kea6_options = { + 'info_refresh_time': 'information-refresh-time', + 'name_server': 'dns-servers', + 'domain_search': 'domain-search', + 'nis_domain': 'nis-domain-name', + 'nis_server': 'nis-servers', + 'nisplus_domain': 'nisp-domain-name', + 'nisplus_server': 'nisp-servers', + 'sntp_server': 'sntp-servers', + 'captive_portal': 'v6-captive-portal' +} + +def kea_parse_options(config): + options = [] + + for node, option_name in kea4_options.items(): + if node not in config: + continue + + value = ", ".join(config[node]) if isinstance(config[node], list) else config[node] + options.append({'name': option_name, 'data': value}) + + if 'client_prefix_length' in config: + options.append({'name': 'subnet-mask', 'data': netmask_from_cidr('0.0.0.0/' + config['client_prefix_length'])}) + + if 'ip_forwarding' in config: + options.append({'name': 'ip-forwarding', 'data': "true"}) + + if 'static_route' in config: + default_route = '' + + if 'default_router' in config: + default_route = isc_static_route('0.0.0.0/0', config['default_router']) + + routes = [isc_static_route(route, route_options['next_hop']) for route, route_options in config['static_route'].items()] + + options.append({'name': 'rfc3442-static-route', 'data': ", ".join(routes if not default_route else routes + [default_route])}) + options.append({'name': 'windows-static-route', 'data': ", ".join(routes)}) + + if 'time_zone' in config: + with open("/usr/share/zoneinfo/" + config['time_zone'], "rb") as f: + tz_string = f.read().split(b"\n")[-2].decode("utf-8") + + options.append({'name': 'pcode', 'data': tz_string}) + options.append({'name': 'tcode', 'data': config['time_zone']}) + + return options + +def kea_parse_subnet(subnet, config): + out = {'subnet': subnet} + options = kea_parse_options(config) + + if 'bootfile_name' in config: + out['boot-file-name'] = config['bootfile_name'] + + if 'bootfile_server' in config: + out['next-server'] = config['bootfile_server'] + + if 'lease' in config: + out['valid-lifetime'] = int(config['lease']) + out['max-valid-lifetime'] = int(config['lease']) + + if 'range' in config: + pools = [] + for num, range_config in config['range'].items(): + start, stop = range_config['start'], range_config['stop'] + pools.append({'pool': f'{start} - {stop}'}) + out['pools'] = pools + + if 'static_mapping' in config: + reservations = [] + for host, host_config in config['static_mapping'].items(): + if 'disable' in host_config: + continue + + reservations.append({ + 'hw-address': host_config['mac_address'], + 'ip-address': host_config['ip_address'] + }) + out['reservations'] = reservations + + unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller') + if unifi_controller: + options.append({ + 'name': 'unifi-controller', + 'data': unifi_controller, + 'space': 'ubnt' + }) + + if options: + out['option-data'] = options + + return out + +def kea6_parse_options(config): + options = [] + + if 'common_options' in config: + common_opt = config['common_options'] + + for node, option_name in kea6_options.items(): + if node not in common_opt: + continue + + value = ", ".join(common_opt[node]) if isinstance(common_opt[node], list) else common_opt[node] + options.append({'name': option_name, 'data': value}) + + for node, option_name in kea6_options.items(): + if node not in config: + continue + + value = ", ".join(config[node]) if isinstance(config[node], list) else config[node] + options.append({'name': option_name, 'data': value}) + + if 'sip_server' in config: + sip_servers = config['sip_server'] + + addrs = [] + hosts = [] + + for server in sip_servers: + if is_ipv6(server): + addrs.append(server) + else: + hosts.append(server) + + if addrs: + options.append({'name': 'sip-server-addr', 'data': ", ".join(addrs)}) + + if hosts: + options.append({'name': 'sip-server-dns', 'data': ", ".join(hosts)}) + + cisco_tftp = dict_search_args(config, 'vendor_option', 'cisco', 'tftp-server') + if cisco_tftp: + options.append({'name': 'tftp-servers', 'code': 2, 'space': 'cisco', 'data': cisco_tftp}) + + return options + +def kea6_parse_subnet(subnet, config): + out = {'subnet': subnet} + options = kea6_parse_options(config) + + if 'address_range' in config: + addr_range = config['address_range'] + pools = [] + + if 'prefix' in addr_range: + for prefix in addr_range['prefix']: + pools.append({'pool': prefix}) + + if 'start' in addr_range: + for start, range_conf in addr_range['start'].items(): + stop = range_conf['stop'] + pools.append({'pool': f'{start} - {stop}'}) + + out['pools'] = pools + + if 'prefix_delegation' in config: + pd_pools = [] + + if 'prefix' in config['prefix_delegation']: + for prefix, pd_conf in config['prefix_delegation']['prefix'].items(): + pd_pools.append({ + 'prefix': prefix, + 'prefix-len': int(pd_conf['prefix_length']), + 'delegated-len': int(pd_conf['delegated_length']) + }) + + out['pd-pools'] = pd_pools + + if 'lease_time' in config: + if 'default' in config['lease_time']: + out['valid-lifetime'] = int(config['lease_time']['default']) + if 'maximum' in config['lease_time']: + out['max-valid-lifetime'] = int(config['lease_time']['maximum']) + if 'minimum' in config['lease_time']: + out['min-valid-lifetime'] = int(config['lease_time']['minimum']) + + if 'static_mapping' in config: + reservations = [] + for host, host_config in config['static_mapping'].items(): + if 'disable' in host_config: + continue + + reservation = {} + + if 'identifier' in host_config: + reservation['duid'] = host_config['identifier'] + + if 'ipv6_address' in host_config: + reservation['ip-addresses'] = [ host_config['ipv6_address'] ] + + if 'ipv6_prefix' in host_config: + reservation['prefixes'] = [ host_config['ipv6_prefix'] ] + + reservations.append(reservation) + + out['reservations'] = reservations + + if options: + out['option-data'] = options + + return out + +def kea_parse_leases(lease_path): + contents = read_file(lease_path) + lines = contents.split("\n") + output = [] + + if len(lines) < 2: + return output + + headers = lines[0].split(",") + + for line in lines[1:]: + line_out = dict(zip(headers, line.split(","))) + + lifetime = int(line_out['valid_lifetime']) + expiry = int(line_out['expire']) + + line_out['start_timestamp'] = datetime.utcfromtimestamp(expiry - lifetime) + line_out['expire_timestamp'] = datetime.utcfromtimestamp(expiry) if expiry else None + + output.append(line_out) + + return output + +def _ctrl_socket_command(path, command, args=None): + if not os.path.exists(path): + return None + + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect(path) + + payload = {'command': command} + if args: + payload['arguments'] = args + + sock.send(bytes(json.dumps(payload), 'utf-8')) + result = b'' + while True: + data = sock.recv(4096) + result += data + if len(data) < 4096: + break + + return json.loads(result.decode('utf-8')) + +def kea_get_active_config(inet): + ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket' + + config = _ctrl_socket_command(ctrl_socket, 'config-get') + + if not config or 'result' not in config or config['result'] != 0: + return None + + return config + +def kea_get_pool_from_subnet_id(config, inet, subnet_id): + shared_networks = dict_search_args(config, 'arguments', f'Dhcp{inet}', 'shared-networks') + + if not shared_networks: + return None + + for network in shared_networks: + if f'subnet{inet}' not in network: + continue + + for subnet in network[f'subnet{inet}']: + if 'id' in subnet and int(subnet['id']) == int(subnet_id): + return network['name'] + + return None diff --git a/python/vyos/load_config.py b/python/vyos/load_config.py new file mode 100644 index 000000000..af563614d --- /dev/null +++ b/python/vyos/load_config.py @@ -0,0 +1,200 @@ +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +"""This module abstracts the loading of a config file into the running +config. It provides several varieties of loading a config file, from the +legacy version to the developing versions, as a means of offering +alternatives for competing use cases, and a base for profiling the +performance of each. +""" + +import sys +from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import Union, Literal, TypeAlias, get_type_hints, get_args + +from vyos.config import Config +from vyos.configtree import ConfigTree, DiffTree +from vyos.configsource import ConfigSourceSession, VyOSError +from vyos.component_version import from_string as version_from_string +from vyos.component_version import from_system as version_from_system +from vyos.migrator import Migrator, VirtualMigrator, MigratorError +from vyos.utils.process import popen, DEVNULL + +Variety: TypeAlias = Literal['explicit', 'batch', 'tree', 'legacy'] +ConfigObj: TypeAlias = Union[str, ConfigTree] + +thismod = sys.modules[__name__] + +class LoadConfigError(Exception): + """Raised when an error occurs loading a config file. + """ + +# utility functions + +def get_running_config(config: Config) -> ConfigTree: + return config.get_config_tree(effective=True) + +def get_proposed_config(config_file: str = None) -> ConfigTree: + config_str = Path(config_file).read_text() + return ConfigTree(config_str) + +def migration_needed(config_obj: ConfigObj) -> bool: + """Check if a migration is needed for the config object. + """ + if not isinstance(config_obj, ConfigTree): + atree = get_proposed_config(config_obj) + else: + atree = config_obj + version_str = atree.get_version_string() + if not version_str: + return True + aversion = version_from_string(version_str.splitlines()[1]) + bversion = version_from_system() + return aversion != bversion + +def check_session(strict: bool, switch: Variety) -> None: + """Check if we are in a config session, with no uncommitted changes, if + strict. This is not needed for legacy load, as these checks are + implicit. + """ + + if switch == 'legacy': + return + + context = ConfigSourceSession() + + if not context.in_session(): + raise LoadConfigError('not in a config session') + + if strict and context.session_changed(): + raise LoadConfigError('commit or discard changes before loading config') + +# methods to call for each variety + +# explicit +def diff_to_commands(ctree: ConfigTree, ntree: ConfigTree) -> list: + """Calculate the diff between the current and proposed config.""" + # Calculate the diff between the current and new config tree + commands = DiffTree(ctree, ntree).to_commands() + # on an empty set of 'add' or 'delete' commands, to_commands + # returns '\n'; prune below + command_list = commands.splitlines() + command_list = [c for c in command_list if c] + return command_list + +def set_commands(cmds: list) -> None: + """Set commands in the config session.""" + if not cmds: + print('no commands to set') + return + error_out = [] + for op in cmds: + out, rc = popen(f'/opt/vyatta/sbin/my_{op}', shell=True, stderr=DEVNULL) + if rc != 0: + error_out.append(out) + continue + if error_out: + out = '\n'.join(error_out) + raise LoadConfigError(out) + +# legacy +class LoadConfig(ConfigSourceSession): + """A subclass for calling 'loadFile'. + """ + def load_config(self, file_name): + return self._run(['/bin/cli-shell-api','loadFile', file_name]) + +# end methods to call for each variety + +def migrate(config_obj: ConfigObj) -> ConfigObj: + """Migrate a config object to the current version. + """ + if isinstance(config_obj, ConfigTree): + config_file = NamedTemporaryFile(delete=False).name + Path(config_file).write_text(config_obj.to_string()) + else: + config_file = config_obj + + virtual_migration = VirtualMigrator(config_file) + migration = Migrator(config_file) + try: + virtual_migration.run() + migration.run() + except MigratorError as e: + raise LoadConfigError(e) from e + else: + if isinstance(config_obj, ConfigTree): + return ConfigTree(Path(config_file).read_text()) + return config_file + finally: + if isinstance(config_obj, ConfigTree): + Path(config_file).unlink() + +def load_explicit(config_obj: ConfigObj): + """Explicit load from file or configtree. + """ + config = Config() + ctree = get_running_config(config) + if isinstance(config_obj, ConfigTree): + ntree = config_obj + else: + ntree = get_proposed_config(config_obj) + # Calculate the diff between the current and proposed config + cmds = diff_to_commands(ctree, ntree) + # Set the commands in the config session + set_commands(cmds) + +def load_batch(config_obj: ConfigObj): + # requires legacy backend patch + raise NotImplementedError('batch loading not implemented') + +def load_tree(config_obj: ConfigObj): + # requires vyconf backend patch + raise NotImplementedError('tree loading not implemented') + +def load_legacy(config_obj: ConfigObj): + """Legacy load from file or configtree. + """ + if isinstance(config_obj, ConfigTree): + config_file = NamedTemporaryFile(delete=False).name + Path(config_file).write_text(config_obj.to_string()) + else: + config_file = config_obj + + config = LoadConfig() + + try: + config.load_config(config_file) + except VyOSError as e: + raise LoadConfigError(e) from e + finally: + if isinstance(config_obj, ConfigTree): + Path(config_file).unlink() + +def load(config_obj: ConfigObj, strict: bool = True, + switch: Variety = 'legacy'): + type_hints = get_type_hints(load) + switch_choice = get_args(type_hints['switch']) + if switch not in switch_choice: + raise ValueError(f'invalid switch: {switch}') + + check_session(strict, switch) + + if migration_needed(config_obj): + config_obj = migrate(config_obj) + + func = getattr(thismod, f'load_{switch}') + func(config_obj) diff --git a/python/vyos/remote.py b/python/vyos/remote.py index fec44b571..b1efcd10b 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -452,7 +452,7 @@ def upload(local_path, urlstring, progressbar=False, source_host='', source_port=0, timeout=10.0): try: progressbar = progressbar and is_interactive() - urlc(urlstring, progressbar, source_host, source_port, timeout).upload(local_path) + urlc(urlstring, progressbar, False, source_host, source_port, timeout).upload(local_path) except Exception as err: print_error(f'Unable to upload "{urlstring}": {err}') except KeyboardInterrupt: diff --git a/python/vyos/system/disk.py b/python/vyos/system/disk.py index f8e0fd1bf..b8a2c0f35 100644 --- a/python/vyos/system/disk.py +++ b/python/vyos/system/disk.py @@ -31,12 +31,17 @@ class DiskDetails: def disk_cleanup(drive_path: str) -> None: """Clean up disk partition table (MBR and GPT) + Remove partition and device signatures. Zeroize primary and secondary headers - first and last 17408 bytes (512 bytes * 34 LBA) on a drive Args: drive_path (str): path to a drive that needs to be cleaned """ + partitions: list[str] = partition_list(drive_path) + for partition in partitions: + run(f'wipefs -af {partition}') + run(f'wipefs -af {drive_path}') run(f'sgdisk -Z {drive_path}') diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py index 0ac16af9a..2692aaea1 100644 --- a/python/vyos/system/grub.py +++ b/python/vyos/system/grub.py @@ -138,6 +138,8 @@ def version_list(root_dir: str = '') -> list[str]: versions_list: list[str] = [] for file in versions_files: versions_list.append(file.stem) + versions_list.sort(reverse=True) + return versions_list diff --git a/python/vyos/system/raid.py b/python/vyos/system/raid.py index 13b99fa69..5b33d34da 100644 --- a/python/vyos/system/raid.py +++ b/python/vyos/system/raid.py @@ -19,7 +19,7 @@ from pathlib import Path from shutil import copy from dataclasses import dataclass -from vyos.utils.process import cmd +from vyos.utils.process import cmd, run from vyos.system import disk @@ -44,18 +44,11 @@ def raid_create(raid_members: list[str], """ raid_devices_num: int = len(raid_members) raid_members_str: str = ' '.join(raid_members) - if Path('/sys/firmware/efi').exists(): - for part in raid_members: - drive: str = disk.partition_parent(part) - command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}' - cmd(command) - else: - for part in raid_members: - drive: str = disk.partition_parent(part) - command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}' - cmd(command) for part in raid_members: - command: str = f'mdadm --zero-superblock {part}' + drive: str = disk.partition_parent(part) + # set partition type GUID for raid member; cf. + # https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs + command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}' cmd(command) command: str = f'mdadm --create /dev/{raid_name} -R --metadata=1.0 \ --raid-devices={raid_devices_num} --level={raid_level} \ @@ -72,6 +65,20 @@ def raid_create(raid_members: list[str], return raid +def clear(): + """Deactivate all RAID arrays""" + command: str = 'mdadm --examine --scan' + raid_config = cmd(command) + if not raid_config: + return + command: str = 'mdadm --run /dev/md?*' + run(command) + command: str = 'mdadm --assemble --scan --auto=yes --symlink=no' + run(command) + command: str = 'mdadm --stop --scan' + run(command) + + def update_initramfs() -> None: """Update initramfs""" mdadm_script = '/etc/initramfs-tools/scripts/local-top/mdadm' diff --git a/python/vyos/template.py b/python/vyos/template.py index 2d4beeec2..f0a50e728 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -791,6 +791,106 @@ def range_to_regex(num_range): regex = range_to_regex(num_range) return f'({regex})' +@register_filter('kea_failover_json') +def kea_failover_json(config): + from json import dumps + + source_addr = config['source_address'] + remote_addr = config['remote'] + + data = { + 'this-server-name': os.uname()[1], + 'mode': 'hot-standby', + 'heartbeat-delay': 10000, + 'max-response-delay': 10000, + 'max-ack-delay': 5000, + 'max-unacked-clients': 0, + 'peers': [ + { + 'name': os.uname()[1], + 'url': f'http://{source_addr}:647/', + 'role': 'standby' if config['status'] == 'secondary' else 'primary', + 'auto-failover': True + }, + { + 'name': config['name'], + 'url': f'http://{remote_addr}:647/', + 'role': 'primary' if config['status'] == 'secondary' else 'standby', + 'auto-failover': True + }] + } + + if 'ca_cert_file' in config: + data['trust-anchor'] = config['ca_cert_file'] + + if 'cert_file' in config: + data['cert-file'] = config['cert_file'] + + if 'cert_key_file' in config: + data['key-file'] = config['cert_key_file'] + + return dumps(data) + +@register_filter('kea_shared_network_json') +def kea_shared_network_json(shared_networks): + from vyos.kea import kea_parse_options + from vyos.kea import kea_parse_subnet + from json import dumps + out = [] + + for name, config in shared_networks.items(): + if 'disable' in config: + continue + + network = { + 'name': name, + 'authoritative': ('authoritative' in config), + 'subnet4': [] + } + options = kea_parse_options(config) + + if 'subnet' in config: + for subnet, subnet_config in config['subnet'].items(): + network['subnet4'].append(kea_parse_subnet(subnet, subnet_config)) + + if options: + network['option-data'] = options + + out.append(network) + + return dumps(out, indent=4) + +@register_filter('kea6_shared_network_json') +def kea6_shared_network_json(shared_networks): + from vyos.kea import kea6_parse_options + from vyos.kea import kea6_parse_subnet + from json import dumps + out = [] + + for name, config in shared_networks.items(): + if 'disable' in config: + continue + + network = { + 'name': name, + 'subnet6': [] + } + options = kea6_parse_options(config) + + if 'interface' in config: + network['interface'] = config['interface'] + + if 'subnet' in config: + for subnet, subnet_config in config['subnet'].items(): + network['subnet6'].append(kea6_parse_subnet(subnet, subnet_config)) + + if options: + network['option-data'] = options + + out.append(network) + + return dumps(out, indent=4) + @register_test('vyos_defined') def vyos_defined(value, test_value=None, var_type=None): """ diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py index 9f27a7fb9..2af87a0ca 100644 --- a/python/vyos/utils/file.py +++ b/python/vyos/utils/file.py @@ -141,6 +141,14 @@ def chmod_2775(path): bitmask = S_ISGID | S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH chmod(path, bitmask) +def chmod_775(path): + """ Make file executable by all """ + from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IXOTH + + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | \ + S_IROTH | S_IXOTH + chmod(path, bitmask) + def makedir(path, user=None, group=None): if os.path.exists(path): return diff --git a/python/vyos/utils/io.py b/python/vyos/utils/io.py index 74099b502..0afaf695c 100644 --- a/python/vyos/utils/io.py +++ b/python/vyos/utils/io.py @@ -26,13 +26,18 @@ def print_error(str='', end='\n'): sys.stderr.write(end) sys.stderr.flush() -def ask_input(question, default='', numeric_only=False, valid_responses=[]): +def ask_input(question, default='', numeric_only=False, valid_responses=[], + no_echo=False): + from getpass import getpass question_out = question if default: question_out += f' (Default: {default})' response = '' while True: - response = input(question_out + ' ').strip() + if not no_echo: + response = input(question_out + ' ').strip() + else: + response = getpass(question_out + ' ').strip() if not response and default: return default if numeric_only: diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 2a0808fca..997ee6309 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -201,6 +201,7 @@ def get_all_vrfs(): return data def interface_list() -> list: + from vyos.ifconfig import Section """ Get list of interfaces in system :rtype: list @@ -519,3 +520,37 @@ def get_vxlan_vni_filter(interface: str) -> list: os_configured_vnis.append(str(vniStart)) return os_configured_vnis + +# Calculate prefix length of an IPv6 range, where possible +# Python-ified from source: https://gitlab.isc.org/isc-projects/dhcp/-/blob/master/keama/confparse.c#L4591 +def ipv6_prefix_length(low, high): + import socket + + bytemasks = [0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe, 0xff] + + try: + lo = bytearray(socket.inet_pton(socket.AF_INET6, low)) + hi = bytearray(socket.inet_pton(socket.AF_INET6, high)) + except: + return None + + xor = bytearray(a ^ b for a, b in zip(lo, hi)) + + plen = 0 + while plen < 128 and xor[plen // 8] == 0: + plen += 8 + + if plen == 128: + return plen + + for i in range((plen // 8) + 1, 16): + if xor[i] != 0: + return None + + for i in range(8): + msk = ~xor[plen // 8] & 0xff + + if msk == bytemasks[i]: + return plen + i + 1 + + return None diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn index e10adbbc6..039a50594 100644 --- a/smoketest/config-tests/dialup-router-medium-vpn +++ b/smoketest/config-tests/dialup-router-medium-vpn @@ -257,7 +257,6 @@ set service dhcp-server shared-network-name LAN authoritative set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 default-router '192.168.0.1' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-name 'vyos.net' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-search 'vyos.net' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 enable-failover set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 lease '86400' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 name-server '192.168.0.1' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic start '192.168.0.200' @@ -268,16 +267,12 @@ set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-map set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping IPTV mac-address '00:50:01:31:b5:f6' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus ip-address '192.168.0.60' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus mac-address '00:50:01:58:ac:95' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 ip-address '192.168.0.109' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 mac-address '00:50:01:bc:ac:51' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 ip-address '192.168.0.11' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 mac-address '00:50:01:70:b9:4d' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 ip-address '192.168.0.12' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 mac-address '00:50:01:70:b7:4f' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 static-mapping-parameters 'option domain-name-servers 192.168.0.6,192.168.0.17;' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV ip-address '192.168.0.101' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV mac-address '00:50:01:ba:62:79' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand ip-address '192.168.0.110' diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos index 78dba3ee2..fca4964bf 100644 --- a/smoketest/configs/basic-vyos +++ b/smoketest/configs/basic-vyos @@ -1,6 +1,7 @@ interfaces { ethernet eth0 { address 192.168.0.1/24 + address fe88::1/56 duplex auto smp-affinity auto speed auto @@ -90,6 +91,23 @@ service { } } } + dhcpv6-server { + shared-network-name LAN6 { + subnet fe88::/56 { + address-range { + prefix fe88::/56 { + temporary + } + } + prefix-delegation { + start fe88:0000:0000:0001:: { + prefix-length 64 + stop fe88:0000:0000:0010:: + } + } + } + } + } dns { forwarding { allow-from 192.168.0.0/16 diff --git a/smoketest/configs/dialup-router-wireguard-ipv6 b/smoketest/configs/dialup-router-wireguard-ipv6 new file mode 100644 index 000000000..33afb9b04 --- /dev/null +++ b/smoketest/configs/dialup-router-wireguard-ipv6 @@ -0,0 +1,1629 @@ +firewall { + all-ping enable + broadcast-ping disable + config-trap disable + group { + address-group DMZ-WEBSERVER { + address 172.16.36.10 + address 172.16.36.40 + address 172.16.36.20 + } + address-group DMZ-RDP-SERVER { + address 172.16.33.40 + } + address-group DOMAIN-CONTROLLER { + address 172.16.100.10 + address 172.16.100.20 + address 172.16.110.30 + } + address-group VIDEO { + address 172.16.33.211 + address 172.16.33.212 + address 172.16.33.213 + address 172.16.33.214 + } + ipv6-network-group LOCAL-ADDRESSES { + network ff02::/64 + network fe80::/10 + } + network-group SSH-IN-ALLOW { + network 100.65.150.0/23 + network 100.64.69.205/32 + network 100.64.8.67/32 + network 100.64.55.1/32 + } + } + ipv6-name ALLOW-ALL-6 { + default-action accept + } + ipv6-name ALLOW-BASIC-6 { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + state { + invalid enable + } + } + rule 10 { + action accept + protocol icmpv6 + } + } + ipv6-name ALLOW-ESTABLISHED-6 { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + state { + invalid enable + } + } + rule 10 { + action accept + destination { + group { + network-group LOCAL-ADDRESSES + } + } + protocol icmpv6 + source { + address fe80::/10 + } + } + rule 20 { + action accept + icmpv6 { + type echo-request + } + protocol icmpv6 + } + rule 21 { + action accept + icmpv6 { + type destination-unreachable + } + protocol icmpv6 + } + rule 22 { + action accept + icmpv6 { + type packet-too-big + } + protocol icmpv6 + } + rule 23 { + action accept + icmpv6 { + type time-exceeded + } + protocol icmpv6 + } + rule 24 { + action accept + icmpv6 { + type parameter-problem + } + protocol icmpv6 + } + } + ipv6-name WAN-LOCAL-6 { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + state { + invalid enable + } + } + rule 10 { + action accept + destination { + address ff02::/64 + } + protocol icmpv6 + source { + address fe80::/10 + } + } + rule 50 { + action accept + destination { + address fe80::/10 + port 546 + } + protocol udp + source { + address fe80::/10 + port 547 + } + } + } + ipv6-receive-redirects disable + ipv6-src-route disable + ip-src-route disable + log-martians enable + name DMZ-GUEST { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + } + name DMZ-LAN { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 100 { + action accept + destination { + group { + address-group DOMAIN-CONTROLLER + } + port 123,389,636 + } + protocol tcp_udp + } + rule 300 { + action accept + destination { + group { + address-group DMZ-RDP-SERVER + } + port 3389 + } + protocol tcp_udp + source { + address 172.16.36.20 + } + } + } + name DMZ-LOCAL { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 50 { + action accept + destination { + address 172.16.254.30 + port 53 + } + protocol tcp_udp + } + rule 123 { + action accept + destination { + port 123 + } + protocol udp + } + } + name DMZ-WAN { + default-action accept + } + name GUEST-DMZ { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + } + name GUEST-LAN { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + } + name GUEST-LOCAL { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 10 { + action accept + destination { + address 172.31.0.254 + port 53 + } + protocol tcp_udp + } + rule 11 { + action accept + destination { + port 67 + } + protocol udp + } + rule 15 { + action accept + destination { + address 172.31.0.254 + } + protocol icmp + } + rule 100 { + action accept + destination { + address 172.31.0.254 + port 80,443 + } + protocol tcp + } + } + name GUEST-WAN { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 25 { + action accept + destination { + port 25,587 + } + protocol tcp + } + rule 53 { + action accept + destination { + port 53 + } + protocol tcp_udp + } + rule 60 { + action accept + source { + address 172.31.0.200 + } + } + rule 80 { + action accept + source { + address 172.31.0.200 + } + } + rule 100 { + action accept + protocol icmp + } + rule 110 { + action accept + destination { + port 110,995 + } + protocol tcp + } + rule 123 { + action accept + destination { + port 123 + } + protocol udp + } + rule 143 { + action accept + destination { + port 143,993 + } + protocol tcp + } + rule 200 { + action accept + destination { + port 80,443 + } + protocol tcp + } + rule 500 { + action accept + destination { + port 500,4500 + } + protocol udp + } + rule 600 { + action accept + destination { + port 5222-5224 + } + protocol tcp + } + rule 601 { + action accept + destination { + port 3478-3497,4500,16384-16387,16393-16402 + } + protocol udp + } + rule 1000 { + action accept + source { + address 172.31.0.184 + } + } + } + name LAN-DMZ { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 22 { + action accept + destination { + port 22 + } + protocol tcp + } + rule 100 { + action accept + destination { + group { + address-group DMZ-WEBSERVER + } + port 22 + } + protocol tcp + } + } + name LAN-GUEST { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + } + name LAN-LOCAL { + default-action accept + } + name LAN-WAN { + default-action accept + rule 90 { + action accept + destination { + address 100.65.150.0/23 + port 25 + } + protocol tcp_udp + source { + group { + address-group VIDEO + } + } + } + rule 100 { + action drop + source { + group { + address-group VIDEO + } + } + } + } + name LOCAL-DMZ { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 100 { + action accept + destination { + address 172.16.36.40 + port 80,443 + } + protocol tcp + } + } + name LOCAL-GUEST { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 5 { + action accept + protocol icmp + } + rule 300 { + action accept + destination { + port 1900 + } + protocol udp + } + } + name LOCAL-LAN { + default-action accept + } + name LOCAL-WAN { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 10 { + action accept + protocol icmp + } + rule 50 { + action accept + destination { + port 53 + } + protocol tcp_udp + } + rule 80 { + action accept + destination { + port 80,443 + } + protocol tcp + } + rule 123 { + action accept + destination { + port 123 + } + protocol udp + } + rule 800 { + action accept + destination { + address 100.65.151.213 + } + protocol udp + } + rule 805 { + action accept + destination { + address 100.65.151.2 + } + protocol all + } + rule 1010 { + action accept + destination { + address 100.64.69.205 + port 7705 + } + protocol udp + source { + port 7705 + } + } + rule 1990 { + action accept + destination { + address 100.64.55.1 + port 10666 + } + protocol udp + } + rule 2000 { + action accept + destination { + address 100.64.39.249 + } + } + rule 10200 { + action accept + destination { + address 100.64.89.98 + port 10200 + } + protocol udp + source { + port 10200 + } + } + } + name WAN-DMZ { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 100 { + action accept + destination { + address 172.16.36.10 + port 80,443 + } + protocol tcp + } + } + name WAN-GUEST { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 1000 { + action accept + destination { + address 172.31.0.184 + } + } + rule 8000 { + action accept + destination { + address 172.31.0.200 + port 10000 + } + protocol udp + } + } + name WAN-LAN { + default-action drop + enable-default-log + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 1000 { + action accept + destination { + address 172.16.33.40 + port 3389 + } + protocol tcp + source { + group { + network-group SSH-IN-ALLOW + } + } + } + } + name WAN-LOCAL { + default-action drop + rule 1 { + action accept + state { + established enable + related enable + } + } + rule 2 { + action drop + log enable + state { + invalid enable + } + } + rule 22 { + action accept + destination { + port 22 + } + protocol tcp + source { + group { + network-group SSH-IN-ALLOW + } + } + } + rule 1990 { + action accept + destination { + port 10666 + } + protocol udp + source { + address 100.64.55.1 + } + } + rule 10000 { + action accept + destination { + port 80,443 + } + protocol tcp + } + rule 10100 { + action accept + destination { + port 10100 + } + protocol udp + source { + port 10100 + } + } + rule 10200 { + action accept + destination { + port 10200 + } + protocol udp + source { + address 100.64.89.98 + port 10200 + } + } + } + options { + interface pppoe0 { + adjust-mss 1452 + adjust-mss6 1432 + } + } + receive-redirects disable + send-redirects enable + source-validation disable + syn-cookies enable + twa-hazards-protection disable +} +interfaces { + dummy dum0 { + address 172.16.254.30/32 + } + ethernet eth0 { + duplex auto + offload { + gro + gso + sg + tso + } + ring-buffer { + rx 256 + tx 256 + } + speed auto + vif 5 { + address 172.16.37.254/24 + ip { + ospf { + authentication { + md5 { + key-id 10 { + md5-key ospf + } + } + } + dead-interval 40 + hello-interval 10 + priority 1 + retransmit-interval 5 + transmit-delay 1 + } + } + } + vif 10 { + address 172.16.33.254/24 + address 172.16.40.254/24 + } + vif 50 { + address 172.16.36.254/24 + } + } + ethernet eth1 { + duplex auto + offload { + gro + gso + sg + tso + } + speed auto + vif 20 { + address 172.31.0.254/24 + } + } + ethernet eth2 { + disable + duplex auto + offload { + gro + gso + sg + tso + } + speed auto + } + ethernet eth3 { + duplex auto + offload { + gro + gso + sg + tso + } + ring-buffer { + rx 256 + tx 256 + } + speed auto + vif 7 { + } + } + loopback lo { + address 172.16.254.30/32 + } + pppoe pppoe0 { + authentication { + password vyos + user vyos + } + default-route force + dhcpv6-options { + pd 0 { + interface eth0.10 { + address 1 + sla-id 10 + } + interface eth1.20 { + address 1 + sla-id 20 + } + length 56 + } + } + ipv6 { + address { + autoconf + } + } + no-peer-dns + source-interface eth3.7 + } + wireguard wg100 { + address 172.16.252.128/31 + mtu 1500 + peer HR6 { + address 100.65.151.213 + allowed-ips 0.0.0.0/0 + port 10100 + pubkey yLpi+UZuI019bmWH2h5fX3gStbpPPPLgEoYMyrdkOnQ= + } + port 10100 + } + wireguard wg200 { + address 172.16.252.130/31 + mtu 1500 + peer WH56 { + address 80.151.69.205 + allowed-ips 0.0.0.0/0 + port 10200 + pubkey XQbkj6vnKKBJfJQyThXysU0iGxCvEOEb31kpaZgkrD8= + } + port 10200 + } + wireguard wg666 { + address 172.29.0.1/31 + mtu 1500 + peer WH34 { + address 100.65.55.1 + allowed-ips 0.0.0.0/0 + port 10666 + pubkey yaTN4+xAafKM04D+Baeg5GWfbdaw35TE9HQivwRgAk0= + } + port 10666 + } +} +nat { + destination { + rule 8000 { + destination { + port 10000 + } + inbound-interface pppoe0 + protocol udp + translation { + address 172.31.0.200 + } + } + } + source { + rule 50 { + outbound-interface pppoe0 + source { + address 100.64.0.0/24 + } + translation { + address masquerade + } + } + rule 100 { + outbound-interface pppoe0 + source { + address 172.16.32.0/21 + } + translation { + address masquerade + } + } + rule 200 { + outbound-interface pppoe0 + source { + address 172.16.100.0/24 + } + translation { + address masquerade + } + } + rule 300 { + outbound-interface pppoe0 + source { + address 172.31.0.0/24 + } + translation { + address masquerade + } + } + rule 400 { + outbound-interface pppoe0 + source { + address 172.18.200.0/21 + } + translation { + address masquerade + } + } + rule 1000 { + destination { + address 192.168.189.0/24 + } + outbound-interface wg666 + source { + address 172.16.32.0/21 + } + translation { + address 172.29.0.1 + } + } + rule 1001 { + destination { + address 192.168.189.0/24 + } + outbound-interface wg666 + source { + address 172.16.100.0/24 + } + translation { + address 172.29.0.1 + } + } + } +} +policy { + route-map MAP-OSPF-CONNECTED { + rule 1 { + action deny + match { + interface eth1.20 + } + } + rule 20 { + action permit + match { + interface eth0.10 + } + } + rule 40 { + action permit + match { + interface eth0.50 + } + } + } +} +protocols { + bfd { + peer 172.16.252.129 { + } + peer 172.16.252.131 { + } + peer 172.18.254.201 { + } + } + bgp 64503 { + address-family { + ipv4-unicast { + network 172.16.32.0/21 { + } + network 172.16.100.0/24 { + } + network 172.16.252.128/31 { + } + network 172.16.252.130/31 { + } + network 172.16.254.30/32 { + } + network 172.18.0.0/16 { + } + } + } + neighbor 172.16.252.129 { + peer-group WIREGUARD + } + neighbor 172.16.252.131 { + peer-group WIREGUARD + } + neighbor 172.18.254.201 { + address-family { + ipv4-unicast { + nexthop-self { + } + } + } + bfd { + } + remote-as 64503 + update-source dum0 + } + parameters { + default { + no-ipv4-unicast + } + log-neighbor-changes + } + peer-group WIREGUARD { + address-family { + ipv4-unicast { + soft-reconfiguration { + inbound + } + } + } + bfd + remote-as external + } + timers { + holdtime 30 + keepalive 10 + } + } + ospf { + area 0 { + network 172.16.254.30/32 + network 172.16.37.0/24 + network 172.18.201.0/24 + network 172.18.202.0/24 + network 172.18.203.0/24 + network 172.18.204.0/24 + } + default-information { + originate { + always + metric-type 2 + } + } + log-adjacency-changes { + detail + } + parameters { + abr-type cisco + router-id 172.16.254.30 + } + passive-interface default + passive-interface-exclude eth0.5 + redistribute { + connected { + metric-type 2 + route-map MAP-OSPF-CONNECTED + } + } + } + static { + interface-route6 2000::/3 { + next-hop-interface pppoe0 { + } + } + route 10.0.0.0/8 { + blackhole { + distance 254 + } + } + route 169.254.0.0/16 { + blackhole { + distance 254 + } + } + route 172.16.0.0/12 { + blackhole { + distance 254 + } + } + route 172.16.32.0/21 { + blackhole { + } + } + route 172.18.0.0/16 { + blackhole { + } + } + route 172.29.0.2/31 { + next-hop 172.29.0.0 { + } + } + route 192.168.0.0/16 { + blackhole { + distance 254 + } + } + route 192.168.189.0/24 { + next-hop 172.29.0.0 { + } + } + } +} +service { + dhcp-server { + shared-network-name BACKBONE { + authoritative + subnet 172.16.37.0/24 { + default-router 172.16.37.254 + domain-name vyos.net + domain-search vyos.net + lease 86400 + name-server 172.16.254.30 + ntp-server 172.16.254.30 + range 0 { + start 172.16.37.120 + stop 172.16.37.149 + } + static-mapping AP1 { + ip-address 172.16.37.231 + mac-address 02:00:00:00:ee:18 + } + static-mapping AP2 { + ip-address 172.16.37.232 + mac-address 02:00:00:00:52:84 + } + static-mapping AP3 { + ip-address 172.16.37.233 + mac-address 02:00:00:00:51:c0 + } + static-mapping AP4 { + ip-address 172.16.37.234 + mac-address 02:00:00:00:e6:fc + } + static-mapping AP5 { + ip-address 172.16.37.235 + mac-address 02:00:00:00:c3:50 + } + } + } + shared-network-name GUEST { + authoritative + subnet 172.31.0.0/24 { + default-router 172.31.0.254 + domain-name vyos.net + domain-search vyos.net + lease 86400 + name-server 172.31.0.254 + range 0 { + start 172.31.0.101 + stop 172.31.0.199 + } + } + } + shared-network-name LAN { + authoritative + subnet 172.16.33.0/24 { + default-router 172.16.33.254 + domain-name vyos.net + domain-search vyos.net + lease 86400 + name-server 172.16.254.30 + ntp-server 172.16.254.30 + range 0 { + start 172.16.33.100 + stop 172.16.33.189 + } + static-mapping one { + ip-address 172.16.33.221 + mac-address 02:00:00:00:eb:a6 + } + static-mapping two { + ip-address 172.16.33.211 + mac-address 02:00:00:00:58:90 + } + static-mapping three { + ip-address 172.16.33.212 + mac-address 02:00:00:00:12:c7 + } + static-mapping four { + ip-address 172.16.33.214 + mac-address 02:00:00:00:c4:33 + } + } + } + } + dns { + dynamic { + interface pppoe0 { + service vyos { + host-name r1.vyos.net + login vyos-vyos + password vyos + protocol dyndns2 + server dyndns.vyos.io + } + } + } + forwarding { + allow-from 172.16.0.0/12 + domain 16.172.in-addr.arpa { + addnta + recursion-desired + server 172.16.100.10 + server 172.16.100.20 + } + domain 18.172.in-addr.arpa { + addnta + recursion-desired + server 172.16.100.10 + server 172.16.100.20 + } + domain vyos.net { + addnta + recursion-desired + server 172.16.100.20 + server 172.16.100.10 + } + ignore-hosts-file + listen-address 172.16.254.30 + listen-address 172.31.0.254 + negative-ttl 60 + } + } + lldp { + legacy-protocols { + cdp + edp + fdp + sonmp + } + snmp { + enable + } + } + router-advert { + interface eth0.10 { + prefix ::/64 { + preferred-lifetime 2700 + valid-lifetime 5400 + } + } + interface eth1.20 { + prefix ::/64 { + preferred-lifetime 2700 + valid-lifetime 5400 + } + } + } + snmp { + community ro-community { + authorization ro + network 172.16.100.0/24 + } + contact "VyOS" + listen-address 172.16.254.30 { + port 161 + } + location "CLOUD" + } + ssh { + disable-host-validation + port 22 + } +} +system { + config-management { + commit-revisions 200 + } + conntrack { + expect-table-size 2048 + hash-size 32768 + modules { + ftp + h323 + nfs + pptp + sqlnet + tftp + } + table-size 262144 + timeout { + icmp 30 + other 600 + udp { + other 300 + stream 300 + } + } + } + console { + device ttyS0 { + speed 115200 + } + } + domain-name vyos.net + host-name r1 + login { + user vyos { + authentication { + encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ + plaintext-password "" + } + } + } + name-server 172.16.254.30 + ntp { + allow-clients { + address 172.16.0.0/12 + } + server time1.vyos.net { + } + server time2.vyos.net { + } + } + option { + ctrl-alt-delete ignore + performance latency + reboot-on-panic + startup-beep + } + syslog { + global { + facility all { + level debug + } + facility protocols { + level debug + } + } + host 172.16.100.1 { + facility all { + level warning + } + } + } + time-zone Europe/Berlin +} +traffic-policy { + shaper QoS { + bandwidth 50mbit + default { + bandwidth 100% + burst 15k + queue-limit 1000 + queue-type fq-codel + } + } +} +zone-policy { + zone DMZ { + default-action drop + from GUEST { + firewall { + name GUEST-DMZ + } + } + from LAN { + firewall { + name LAN-DMZ + } + } + from LOCAL { + firewall { + name LOCAL-DMZ + } + } + from WAN { + firewall { + name WAN-DMZ + } + } + interface eth0.50 + } + zone GUEST { + default-action drop + from DMZ { + firewall { + name DMZ-GUEST + } + } + from LAN { + firewall { + name LAN-GUEST + } + } + from LOCAL { + firewall { + ipv6-name ALLOW-ALL-6 + name LOCAL-GUEST + } + } + from WAN { + firewall { + ipv6-name ALLOW-ESTABLISHED-6 + name WAN-GUEST + } + } + interface eth1.20 + } + zone LAN { + default-action drop + from DMZ { + firewall { + name DMZ-LAN + } + } + from GUEST { + firewall { + name GUEST-LAN + } + } + from LOCAL { + firewall { + ipv6-name ALLOW-ALL-6 + name LOCAL-LAN + } + } + from WAN { + firewall { + ipv6-name ALLOW-ESTABLISHED-6 + name WAN-LAN + } + } + interface eth0.5 + interface eth0.10 + interface wg100 + interface wg200 + } + zone LOCAL { + default-action drop + from DMZ { + firewall { + name DMZ-LOCAL + } + } + from GUEST { + firewall { + ipv6-name ALLOW-ESTABLISHED-6 + name GUEST-LOCAL + } + } + from LAN { + firewall { + ipv6-name ALLOW-ALL-6 + name LAN-LOCAL + } + } + from WAN { + firewall { + ipv6-name WAN-LOCAL-6 + name WAN-LOCAL + } + } + local-zone + } + zone WAN { + default-action drop + from DMZ { + firewall { + name DMZ-WAN + } + } + from GUEST { + firewall { + ipv6-name ALLOW-ALL-6 + name GUEST-WAN + } + } + from LAN { + firewall { + ipv6-name ALLOW-ALL-6 + name LAN-WAN + } + } + from LOCAL { + firewall { + ipv6-name ALLOW-ALL-6 + name LOCAL-WAN + } + } + interface pppoe0 + interface wg666 + } +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:container@1:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.4 diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 71e2142f9..97dab255e 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -1133,5 +1133,20 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' mpls bgp forwarding', frrconfig) self.cli_delete(['interfaces', 'ethernet', interface, 'vrf']) + def test_bgp_24_srv6_sid(self): + locator_name = 'VyOS_foo' + sid = 'auto' + + self.cli_set(base_path + ['srv6', 'locator', locator_name]) + self.cli_set(base_path + ['sid', 'vpn', 'per-vrf', 'export', sid]) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' segment-routing srv6', frrconfig) + self.assertIn(f' locator {locator_name}', frrconfig) + self.assertIn(f' sid vpn per-vrf export {sid}', frrconfig) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_segment_routing.py b/smoketest/scripts/cli/test_protocols_segment_routing.py new file mode 100755 index 000000000..81d42b925 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_segment_routing.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.utils.process import cmd +from vyos.utils.process import process_named_running + +base_path = ['protocols', 'segment-routing'] +PROCESS_NAME = 'zebra' + +class TestProtocolsSegmentRouting(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsSegmentRouting, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + + def test_srv6(self): + locators = { + 'foo' : { 'prefix' : '2001:a::/64' }, + 'foo' : { 'prefix' : '2001:b::/64', 'usid' : {} }, + } + + for locator, locator_config in locators.items(): + self.cli_set(base_path + ['srv6', 'locator', locator, 'prefix', locator_config['prefix']]) + if 'usid' in locator_config: + self.cli_set(base_path + ['srv6', 'locator', locator, 'behavior-usid']) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'segment-routing', daemon='zebra') + self.assertIn(f'segment-routing', frrconfig) + self.assertIn(f' srv6', frrconfig) + self.assertIn(f' locators', frrconfig) + for locator, locator_config in locators.items(): + self.assertIn(f' locator {locator}', frrconfig) + self.assertIn(f' prefix {locator_config["prefix"]} block-len 40 node-len 24 func-bits 16', frrconfig) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 093e43494..9f6e05ff3 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -14,11 +14,15 @@ # 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 unittest +from json import loads + from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.utils.dict import dict_search_recursive from vyos.utils.process import process_named_running from vyos.utils.file import read_file from vyos.template import address_from_cidr @@ -26,8 +30,10 @@ from vyos.template import inc_ip from vyos.template import dec_ip from vyos.template import netmask_from_cidr -PROCESS_NAME = 'dhcpd' -DHCPD_CONF = '/run/dhcp-server/dhcpd.conf' +PROCESS_NAME = 'kea-dhcp4' +CTRL_PROCESS_NAME = 'kea-ctrl-agent' +KEA4_CONF = '/run/kea/kea-dhcp4.conf' +KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket' base_path = ['service', 'dhcp-server'] subnet = '192.0.2.0/25' router = inc_ip(subnet, 1) @@ -52,6 +58,36 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + def walk_path(self, obj, path): + current = obj + + for i, key in enumerate(path): + if isinstance(key, str): + self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}') + self.assertTrue(key in current, msg=f'Failed path: {path}') + elif isinstance(key, int): + self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}') + self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}') + else: + assert False, "Invalid type" + + current = current[key] + + return current + + def verify_config_object(self, obj, path, value): + base_obj = self.walk_path(obj, path) + self.assertTrue(isinstance(base_obj, list)) + self.assertTrue(any(True for v in base_obj if v == value)) + + def verify_config_value(self, obj, path, key, value): + base_obj = self.walk_path(obj, path) + if isinstance(base_obj, list): + self.assertTrue(any(True for v in base_obj if key in v and v[key] == value)) + elif isinstance(base_obj, dict): + self.assertTrue(key in base_obj) + self.assertEqual(base_obj[key], value) + def test_dhcp_single_pool_range(self): shared_net_name = 'SMOKE-1' @@ -60,15 +96,12 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_1_start = inc_ip(subnet, 40) range_1_stop = inc_ip(subnet, 50) - self.cli_set(base_path + ['dynamic-dns-update']) - pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway self.cli_set(pool + ['default-router', router]) self.cli_set(pool + ['name-server', dns_1]) self.cli_set(pool + ['name-server', dns_2]) self.cli_set(pool + ['domain-name', domain_name]) - self.cli_set(pool + ['ping-check']) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): @@ -81,20 +114,37 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) - network = address_from_cidr(subnet) - netmask = netmask_from_cidr(subnet) - self.assertIn(f'ddns-update-style interim;', config) - self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config) - self.assertIn(f'option domain-name-servers {dns_1}, {dns_2};', config) - self.assertIn(f'option routers {router};', config) - self.assertIn(f'option domain-name "{domain_name}";', config) - self.assertIn(f'default-lease-time 86400;', config) - self.assertIn(f'max-lease-time 86400;', config) - self.assertIn(f'ping-check true;', config) - self.assertIn(f'range {range_0_start} {range_0_stop};', config) - self.assertIn(f'range {range_1_start} {range_1_stop};', config) - self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_1_start} - {range_1_stop}'}) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -134,6 +184,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) self.cli_set(pool + ['ipv6-only-preferred', ipv6_only_preferred]) + self.cli_set(pool + ['time-zone', 'Europe/London']) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): @@ -144,38 +195,89 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) - - network = address_from_cidr(subnet) - netmask = netmask_from_cidr(subnet) - self.assertIn(f'ddns-update-style none;', config) - self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config) - self.assertIn(f'option domain-name-servers {dns_1}, {dns_2};', config) - self.assertIn(f'option routers {router};', config) - self.assertIn(f'option domain-name "{domain_name}";', config) - - search = '"' + ('", "').join(search_domains) + '"' - self.assertIn(f'option domain-search {search};', config) - - self.assertIn(f'option ip-forwarding true;', config) - self.assertIn(f'option smtp-server {smtp_server};', config) - self.assertIn(f'option pop-server {smtp_server};', config) - self.assertIn(f'option time-servers {time_server};', config) - self.assertIn(f'option wpad-url "{wpad}";', config) - self.assertIn(f'option dhcp-server-identifier {server_identifier};', config) - self.assertIn(f'option tftp-server-name "{tftp_server}";', config) - self.assertIn(f'option bootfile-name "{bootfile_name}";', config) - self.assertIn(f'filename "{bootfile_name}";', config) - self.assertIn(f'next-server {bootfile_server};', config) - self.assertIn(f'default-lease-time 86400;', config) - self.assertIn(f'max-lease-time 86400;', config) - self.assertIn(f'range {range_0_start} {range_0_stop};', config) - self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) - self.assertIn(f'option rfc8925-ipv6-only-preferred {ipv6_only_preferred};', config) - - # weird syntax for those static routes - self.assertIn(f'option rfc3442-static-route 24,10,0,0,192,0,2,1, 0,192,0,2,1;', config) - self.assertIn(f'option windows-static-route 24,10,0,0,192,0,2,1;', config) + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'boot-file-name', bootfile_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'next-server', bootfile_server) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-search', 'data': ', '.join(search_domains)}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'pop-server', 'data': smtp_server}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'smtp-server', 'data': smtp_server}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'time-servers', 'data': time_server}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'dhcp-server-identifier', 'data': server_identifier}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'tftp-server-name', 'data': tftp_server}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'wpad-url', 'data': wpad}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'rfc3442-static-route', 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'v6-only-preferred', 'data': ipv6_only_preferred}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'ip-forwarding', 'data': "true"}) + + # Time zone + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'tcode', 'data': 'Europe/London'}) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -205,27 +307,39 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) - network = address_from_cidr(subnet) - netmask = netmask_from_cidr(subnet) - self.assertIn(f'ddns-update-style none;', config) - self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config) - self.assertIn(f'option domain-name-servers {dns_1}, {dns_2};', config) - self.assertIn(f'option routers {router};', config) - self.assertIn(f'option domain-name "{domain_name}";', config) - self.assertIn(f'default-lease-time 86400;', config) - self.assertIn(f'max-lease-time 86400;', config) + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}) client_base = 10 for client in ['client1', 'client2', 'client3']: mac = '00:50:00:00:00:{}'.format(client_base) ip = inc_ip(subnet, client_base) - self.assertIn(f'host {shared_net_name}_{client}' + ' {', config) - self.assertIn(f'fixed-address {ip};', config) - self.assertIn(f'hardware ethernet {mac};', config) - client_base += 1 - self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'], + {'hw-address': mac, 'ip-address': ip}) + + client_base += 1 # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -266,7 +380,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) + config = read_file(KEA4_CONF) + obj = loads(config) + for network in ['0', '1', '2', '3']: shared_net_name = f'VyOS-SMOKETEST-{network}' subnet = f'192.0.{network}.0/24' @@ -278,27 +394,43 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_1_start = inc_ip(subnet, 30) range_1_stop = inc_ip(subnet, 40) - network = address_from_cidr(subnet) - netmask = netmask_from_cidr(subnet) - - self.assertIn(f'ddns-update-style none;', config) - self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config) - self.assertIn(f'option domain-name-servers {dns_1};', config) - self.assertIn(f'option routers {router};', config) - self.assertIn(f'option domain-name "{domain_name}";', config) - self.assertIn(f'default-lease-time {lease_time};', config) - self.assertIn(f'max-lease-time {lease_time};', config) - self.assertIn(f'range {range_0_start} {range_0_stop};', config) - self.assertIn(f'range {range_1_start} {range_1_stop};', config) - self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'valid-lifetime', int(lease_time)) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'max-valid-lifetime', int(lease_time)) + + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': dns_1}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}) + + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'], + {'pool': f'{range_1_start} - {range_1_stop}'}) client_base = 60 for client in ['client1', 'client2', 'client3', 'client4']: mac = '02:50:00:00:00:{}'.format(client_base) ip = inc_ip(subnet, client_base) - self.assertIn(f'host {shared_net_name}_{client}' + ' {', config) - self.assertIn(f'fixed-address {ip};', config) - self.assertIn(f'hardware ethernet {mac};', config) + + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'], + {'hw-address': mac, 'ip-address': ip}) + client_base += 1 # Check for running process @@ -319,14 +451,23 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - # VErify - config = read_file(DHCPD_CONF) - network = address_from_cidr(subnet) - netmask = netmask_from_cidr(subnet) + config = read_file(KEA4_CONF) + obj = loads(config) - self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config) - self.assertIn(f'option routers {router};', config) - self.assertIn(f'range {range_0_start} {range_0_stop};', config) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -352,15 +493,27 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - # Verify - config = read_file(DHCPD_CONF) - network = address_from_cidr(subnet) - netmask = netmask_from_cidr(subnet) + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) - self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config) - self.assertIn(f'option routers {router};', config) - self.assertIn(f'range {range_0_start} {range_0_stop_excl};', config) - self.assertIn(f'range {range_0_start_excl} {range_0_stop};', config) + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}) + + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop_excl}'}) + + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start_excl} - {range_0_stop}'}) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -384,41 +537,23 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) - network = address_from_cidr(subnet) - netmask = netmask_from_cidr(subnet) - # Check the relay network - self.assertIn(f'subnet {network} netmask {netmask}' + r' { }', config) - - relay_network = address_from_cidr(relay_subnet) - relay_netmask = netmask_from_cidr(relay_subnet) - self.assertIn(f'subnet {relay_network} netmask {relay_netmask}' + r' {', config) - self.assertIn(f'option routers {relay_router};', config) - self.assertIn(f'range {range_0_start} {range_0_stop};', config) - - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - def test_dhcp_invalid_raw_options(self): - shared_net_name = 'SMOKE-5' + config = read_file(KEA4_CONF) + obj = loads(config) - range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY') + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet) - pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] - # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['range', '0', 'start', range_0_start]) - self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': relay_router}) - self.cli_set(base_path + ['global-parameters', 'this-is-crap']) - # check generate() - dhcpd should not acceot this garbage config - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_delete(base_path + ['global-parameters']) - - # commit changes - self.cli_commit() + # Verify pools + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -449,41 +584,43 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['failover', 'remote', failover_remote]) self.cli_set(base_path + ['failover', 'status', 'primary']) - # check validate() - failover needs to be enabled for at least one subnet - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_set(pool + ['enable-failover']) - # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) - - self.assertIn(f'failover peer "{failover_name}"' + r' {', config) - self.assertIn(f'primary;', config) - self.assertIn(f'mclt 1800;', config) - self.assertIn(f'mclt 1800;', config) - self.assertIn(f'split 128;', config) - self.assertIn(f'port 647;', config) - self.assertIn(f'peer port 647;', config) - self.assertIn(f'max-response-delay 30;', config) - self.assertIn(f'max-unacked-updates 10;', config) - self.assertIn(f'load balance max seconds 3;', config) - self.assertIn(f'address {failover_local};', config) - self.assertIn(f'peer address {failover_remote};', config) - - network = address_from_cidr(subnet) - netmask = netmask_from_cidr(subnet) - self.assertIn(f'ddns-update-style none;', config) - self.assertIn(f'subnet {network} netmask {netmask}' + r' {', config) - self.assertIn(f'option routers {router};', config) - self.assertIn(f'range {range_0_start} {range_0_stop};', config) - self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) - self.assertIn(f'failover peer "{failover_name}";', config) - self.assertIn(f'deny dynamic bootp clients;', config) + config = read_file(KEA4_CONF) + obj = loads(config) + + # Verify failover + self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL) + + self.verify_config_object( + obj, + ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], + {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'primary', 'auto-failover': True}) + + self.verify_config_object( + obj, + ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'], + {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'standby', 'auto-failover': True}) + + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'], + {'name': 'routers', 'data': router}) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], + {'pool': f'{range_0_start} - {range_0_stop}'}) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertTrue(process_named_running(CTRL_PROCESS_NAME)) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 4d9dabc3f..175a67537 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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,8 @@ import unittest +from json import loads + from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError @@ -23,8 +25,8 @@ from vyos.template import inc_ip from vyos.utils.process import process_named_running from vyos.utils.file import read_file -PROCESS_NAME = 'dhcpd' -DHCPD_CONF = '/run/dhcp-server/dhcpdv6.conf' +PROCESS_NAME = 'kea-dhcp6' +KEA6_CONF = '/run/kea/kea-dhcp6.conf' base_path = ['service', 'dhcpv6-server'] subnet = '2001:db8:f00::/64' @@ -52,6 +54,36 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + def walk_path(self, obj, path): + current = obj + + for i, key in enumerate(path): + if isinstance(key, str): + self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}') + self.assertTrue(key in current, msg=f'Failed path: {path}') + elif isinstance(key, int): + self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}') + self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}') + else: + assert False, "Invalid type" + + current = current[key] + + return current + + def verify_config_object(self, obj, path, value): + base_obj = self.walk_path(obj, path) + self.assertTrue(isinstance(base_obj, list)) + self.assertTrue(any(True for v in base_obj if v == value)) + + def verify_config_value(self, obj, path, key, value): + base_obj = self.walk_path(obj, path) + if isinstance(base_obj, list): + self.assertTrue(any(True for v in base_obj if key in v and v[key] == value)) + elif isinstance(base_obj, dict): + self.assertTrue(key in base_obj) + self.assertEqual(base_obj[key], value) + def test_single_pool(self): shared_net_name = 'SMOKE-1' search_domains = ['foo.vyos.net', 'bar.vyos.net'] @@ -99,34 +131,66 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) - self.assertIn(f'option dhcp6.preference {preference};', config) - - self.assertIn(f'subnet6 {subnet}' + r' {', config) - search = '"' + '", "'.join(search_domains) + '"' - nissrv = ', '.join(nis_servers) - self.assertIn(f'range6 {range_start} {range_stop};', config) - self.assertIn(f'default-lease-time {lease_time};', config) - self.assertIn(f'default-lease-time {lease_time};', config) - self.assertIn(f'max-lease-time {max_lease_time};', config) - self.assertIn(f'min-lease-time {min_lease_time};', config) - self.assertIn(f'option dhcp6.domain-search {search};', config) - self.assertIn(f'option dhcp6.name-servers {dns_1}, {dns_2};', config) - self.assertIn(f'option dhcp6.nis-domain-name "{domain}";', config) - self.assertIn(f'option dhcp6.nis-servers {nissrv};', config) - self.assertIn(f'option dhcp6.nisp-domain-name "{domain}";', config) - self.assertIn(f'option dhcp6.nisp-servers {nissrv};', config) - self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) + config = read_file(KEA6_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'valid-lifetime', int(lease_time)) + self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'min-valid-lifetime', int(min_lease_time)) + self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'max-valid-lifetime', int(max_lease_time)) + + # Verify options + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'domain-search', 'data': ", ".join(search_domains)}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'nis-domain-name', 'data': domain}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'nis-servers', 'data': ", ".join(nis_servers)}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'nisp-domain-name', 'data': domain}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'nisp-servers', 'data': ", ".join(nis_servers)}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'sntp-servers', 'data': sntp_server}) + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'], + {'name': 'sip-server-dns', 'data': sip_server}) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'], + {'pool': f'{range_start} - {range_stop}'}) client_base = 1 for client in ['client1', 'client2', 'client3']: cid = '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{}'.format(client_base) ip = inc_ip(subnet, client_base) prefix = inc_ip(subnet, client_base << 64) + '/64' - self.assertIn(f'host {shared_net_name}_{client}' + ' {', config) - self.assertIn(f'fixed-address6 {ip};', config) - self.assertIn(f'fixed-prefix6 {prefix};', config) - self.assertIn(f'host-identifier option dhcp6.client-id {cid};', config) + + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'reservations'], + {'duid': cid, 'ip-addresses': [ip], 'prefixes': [prefix]}) + client_base += 1 # Check for running process @@ -138,22 +202,34 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): range_start = inc_ip(subnet, 256) # ::100 range_stop = inc_ip(subnet, 65535) # ::ffff delegate_start = '2001:db8:ee::' - delegate_stop = '2001:db8:ee:ff00::' - delegate_len = '56' + delegate_len = '64' + prefix_len = '56' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['address-range', 'start', range_start, 'stop', range_stop]) - self.cli_set(pool + ['prefix-delegation', 'start', delegate_start, 'stop', delegate_stop]) - self.cli_set(pool + ['prefix-delegation', 'start', delegate_start, 'prefix-length', delegate_len]) + self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'delegated-length', delegate_len]) + self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'prefix-length', prefix_len]) # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) - self.assertIn(f'subnet6 {subnet}' + r' {', config) - self.assertIn(f'range6 {range_start} {range_stop};', config) - self.assertIn(f'prefix6 {delegate_start} {delegate_stop} /{delegate_len};', config) + config = read_file(KEA6_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) + + # Verify pools + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'], + {'pool': f'{range_start} - {range_stop}'}) + + self.verify_config_object( + obj, + ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pd-pools'], + {'prefix': delegate_start, 'prefix-len': int(prefix_len), 'delegated-len': int(delegate_len)}) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -170,10 +246,16 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - config = read_file(DHCPD_CONF) - self.assertIn(f'option dhcp6.name-servers {ns_global_1}, {ns_global_2};', config) - self.assertIn(f'subnet6 {subnet}' + r' {', config) - self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) + config = read_file(KEA6_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) + + self.verify_config_object( + obj, + ['Dhcp6', 'option-data'], + {'name': 'dns-servers', "code": 23, "space": "dhcp6", "csv-format": True, 'data': f'{ns_global_1}, {ns_global_2}'}) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index cb3d90593..ae46b18ba 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -17,8 +17,6 @@ import os import unittest import tempfile -import random -import string from base_vyostest_shim import VyOSUnitTestSHIM @@ -67,14 +65,12 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_set(name_path + [svc, 'address', interface]) self.cli_set(name_path + [svc, 'host-name', hostname]) self.cli_set(name_path + [svc, 'password', password]) - self.cli_set(name_path + [svc, 'zone', zone]) - self.cli_set(name_path + [svc, 'ttl', ttl]) for opt, value in details.items(): self.cli_set(name_path + [svc, opt, value]) - # 'zone' option is supported and required by 'cloudfare', but not 'freedns' and 'zoneedit' + # 'zone' option is supported by 'cloudfare' and 'zoneedit1', but not 'freedns' self.cli_set(name_path + [svc, 'zone', zone]) - if details['protocol'] == 'cloudflare': + if details['protocol'] in ['cloudflare', 'zoneedit1']: pass else: # exception is raised for unsupported ones @@ -292,9 +288,38 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'password=\'{password}\'', ddclient_conf) self.assertIn(f'{hostname}', ddclient_conf) - def test_07_dyndns_vrf(self): + def test_07_dyndns_dynamic_interface(self): + # Check if DDNS service can be configured and runs + svc_path = name_path + ['namecheap'] + proto = 'namecheap' + dyn_interface = 'pppoe587' + + self.cli_set(svc_path + ['address', dyn_interface]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['server', server]) + self.cli_set(svc_path + ['username', username]) + self.cli_set(svc_path + ['password', password]) + self.cli_set(svc_path + ['host-name', hostname]) + + # Dynamic interface will raise a warning but still go through + # XXX: We should have idiomatic class "ConfigSessionWarning" wrapping + # "Warning" similar to "ConfigSessionError". + # with self.assertWarns(Warning): + # self.cli_commit() + self.cli_commit() + + # Check the generating config parameters + ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') + self.assertIn(f'ifv4={dyn_interface}', ddclient_conf) + self.assertIn(f'protocol={proto}', ddclient_conf) + self.assertIn(f'server={server}', ddclient_conf) + self.assertIn(f'login={username}', ddclient_conf) + self.assertIn(f'password=\'{password}\'', ddclient_conf) + self.assertIn(f'{hostname}', ddclient_conf) + + def test_08_dyndns_vrf(self): # Table number randomized, but should be within range 100-65535 - vrf_table = "".join(random.choices(string.digits, k=4)) + vrf_table = '58710' vrf_name = f'vyos-test-{vrf_table}' svc_path = name_path + ['cloudflare'] proto = 'cloudflare' diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 6cb91bcf1..703e3e8c4 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -23,6 +23,7 @@ from urllib3.exceptions import InsecureRequestWarning from base_vyostest_shim import VyOSUnitTestSHIM from base_vyostest_shim import ignore_warning from vyos.utils.file import read_file +from vyos.utils.process import call from vyos.utils.process import process_named_running from vyos.configsession import ConfigSessionError @@ -51,6 +52,23 @@ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww """ +# to test load config via HTTP URL +nginx_conf_smoketest = """ +server { + listen 8000; + server_name localhost; + + root /tmp; + + index index.html; + + location / { + try_files $uri $uri/ =404; + autoindex on; + } +} +""" + PROCESS_NAME = 'nginx' class TestHTTPSService(VyOSUnitTestSHIM.TestCase): @@ -375,6 +393,57 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): r = request('POST', url, verify=False, headers=headers, data=payload) self.assertEqual(r.status_code, 200) + @ignore_warning(InsecureRequestWarning) + def test_api_config_file_load_http(self): + """Test load config from HTTP URL + """ + address = '127.0.0.1' + key = 'VyOS-key' + url = f'https://{address}/config-file' + url_config = f'https://{address}/configure' + headers = {} + tmp_file = 'tmp-config.boot' + nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest' + + self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_commit() + + # load config via HTTP requires nginx config + call(f'sudo touch {nginx_tmp_site}') + call(f'sudo chown vyos:vyattacfg {nginx_tmp_site}') + call(f'sudo chmod +w {nginx_tmp_site}') + + with open(nginx_tmp_site, 'w') as f: + f.write(nginx_conf_smoketest) + call('sudo nginx -s reload') + + # save config + payload = { + 'data': '{"op": "save", "file": "/tmp/tmp-config.boot"}', + 'key': f'{key}', + } + r = request('POST', url, verify=False, headers=headers, data=payload) + self.assertEqual(r.status_code, 200) + + # change config + payload = { + 'data': '{"op": "set", "path": ["interfaces", "dummy", "dum1", "address", "192.0.2.31/32"]}', + 'key': f'{key}', + } + r = request('POST', url_config, verify=False, headers=headers, data=payload) + self.assertEqual(r.status_code, 200) + + # load config from URL + payload = { + 'data': '{"op": "load", "file": "http://localhost:8000/tmp-config.boot"}', + 'key': f'{key}', + } + r = request('POST', url, verify=False, headers=headers, data=payload) + self.assertEqual(r.status_code, 200) + + # cleanup tmp nginx conf + call(f'sudo rm -rf {nginx_tmp_site}') + call('sudo nginx -s reload') if __name__ == '__main__': unittest.main(verbosity=5) diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index ac7d95632..66f7c8057 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -21,10 +21,16 @@ from ipaddress import ip_network from netaddr import IPAddress from netaddr import IPRange from sys import exit +from time import sleep from vyos.config import Config +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key from vyos.template import render from vyos.utils.dict import dict_search +from vyos.utils.dict import dict_search_args +from vyos.utils.file import chmod_775 +from vyos.utils.file import write_file from vyos.utils.process import call from vyos.utils.process import run from vyos.utils.network import is_subnet_connected @@ -33,8 +39,14 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = '/run/dhcp-server/dhcpd.conf' -systemd_override = r'/run/systemd/system/isc-dhcp-server.service.d/10-override.conf' +ctrl_config_file = '/run/kea/kea-ctrl-agent.conf' +ctrl_socket = '/run/kea/dhcp4-ctrl-socket' +config_file = '/run/kea/kea-dhcp4.conf' +lease_file = '/config/dhcp4.leases' + +ca_cert_file = '/run/kea/kea-failover-ca.pem' +cert_file = '/run/kea/kea-failover.pem' +cert_key_file = '/run/kea/kea-failover-key.pem' def dhcp_slice_range(exclude_list, range_dict): """ @@ -130,6 +142,9 @@ def get_config(config=None): dhcp['shared_network_name'][network]['subnet'][subnet].update( {'range' : new_range_dict}) + if dict_search('failover.certificate', dhcp): + dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + return dhcp def verify(dhcp): @@ -166,13 +181,6 @@ def verify(dhcp): if 'next_hop' not in route_option: raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!') - # DHCP failover needs at least one subnet that uses it - if 'enable_failover' in subnet_config: - if 'failover' not in dhcp: - raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \ - 'Failover is not configured globally!') - failover_ok = True - # Check if DHCP address range is inside configured subnet declaration if 'range' in subnet_config: networks = [] @@ -249,14 +257,34 @@ def verify(dhcp): raise ConfigError(f'At least one shared network must be active!') if 'failover' in dhcp: - if not failover_ok: - raise ConfigError('DHCP failover must be enabled for at least one subnet!') - for key in ['name', 'remote', 'source_address', 'status']: if key not in dhcp['failover']: tmp = key.replace('_', '-') raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!') + if len({'certificate', 'ca_certificate'} & set(dhcp['failover'])) == 1: + raise ConfigError(f'DHCP secured failover requires both certificate and CA certificate') + + if 'certificate' in dhcp['failover']: + cert_name = dhcp['failover']['certificate'] + + if cert_name not in dhcp['pki']['certificate']: + raise ConfigError(f'Invalid certificate specified for DHCP failover') + + if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'): + raise ConfigError(f'Invalid certificate specified for DHCP failover') + + if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'): + raise ConfigError(f'Missing private key on certificate specified for DHCP failover') + + if 'ca_certificate' in dhcp['failover']: + ca_cert_name = dhcp['failover']['ca_certificate'] + if ca_cert_name not in dhcp['pki']['ca']: + raise ConfigError(f'Invalid CA certificate specified for DHCP failover') + + if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'): + raise ConfigError(f'Invalid CA certificate specified for DHCP failover') + for address in (dict_search('listen_address', dhcp) or []): if is_addr_assigned(address): listen_ok = True @@ -278,43 +306,71 @@ def generate(dhcp): if not dhcp or 'disable' in dhcp: return None - # Please see: https://vyos.dev/T1129 for quoting of the raw - # parameters we can pass to ISC DHCPd - tmp_file = '/tmp/dhcpd.conf' - render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp, - formater=lambda _: _.replace(""", '"')) - # XXX: as we have the ability for a user to pass in "raw" options via VyOS - # CLI (see T3544) we now ask ISC dhcpd to test the newly rendered - # configuration - tmp = run(f'/usr/sbin/dhcpd -4 -q -t -cf {tmp_file}') - if tmp > 0: - if os.path.exists(tmp_file): - os.unlink(tmp_file) - raise ConfigError('Configuration file errors encountered - check your options!') - - # Now that we know that the newly rendered configuration is "good" we can - # render the "real" configuration - render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp, - formater=lambda _: _.replace(""", '"')) - render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp) - - # Clean up configuration test file - if os.path.exists(tmp_file): - os.unlink(tmp_file) + dhcp['lease_file'] = lease_file + dhcp['machine'] = os.uname().machine + + if not os.path.exists(lease_file): + write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755) + + for f in [cert_file, cert_key_file, ca_cert_file]: + if os.path.exists(f): + os.unlink(f) + + if 'failover' in dhcp: + if 'certificate' in dhcp['failover']: + cert_name = dhcp['failover']['certificate'] + cert_data = dhcp['pki']['certificate'][cert_name]['certificate'] + key_data = dhcp['pki']['certificate'][cert_name]['private']['key'] + write_file(cert_file, wrap_certificate(cert_data), user='_kea', mode=0o600) + write_file(cert_key_file, wrap_private_key(key_data), user='_kea', mode=0o600) + + dhcp['failover']['cert_file'] = cert_file + dhcp['failover']['cert_key_file'] = cert_key_file + + if 'ca_certificate' in dhcp['failover']: + ca_cert_name = dhcp['failover']['ca_certificate'] + ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate'] + write_file(ca_cert_file, wrap_certificate(ca_cert_data), user='_kea', mode=0o600) + + dhcp['failover']['ca_cert_file'] = ca_cert_file + + render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp) + render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp) return None def apply(dhcp): - call('systemctl daemon-reload') - # bail out early - looks like removal from running config + services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server'] + if not dhcp or 'disable' in dhcp: - call('systemctl stop isc-dhcp-server.service') + for service in services: + call(f'systemctl stop {service}.service') + if os.path.exists(config_file): os.unlink(config_file) return None - call('systemctl restart isc-dhcp-server.service') + for service in services: + action = 'restart' + + if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp: + action = 'stop' + + if service == 'kea-ctrl-agent' and 'failover' not in dhcp: + action = 'stop' + + call(f'systemctl {action} {service}.service') + + # op-mode needs ctrl socket permission change + i = 0 + while not os.path.exists(ctrl_socket): + if i > 15: + break + i += 1 + sleep(1) + chmod_775(ctrl_socket) + return None if __name__ == '__main__': diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 427001609..73a708ff5 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 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 @@ -19,18 +19,23 @@ import os from ipaddress import ip_address from ipaddress import ip_network from sys import exit +from time import sleep from vyos.config import Config from vyos.template import render from vyos.template import is_ipv6 from vyos.utils.process import call +from vyos.utils.file import chmod_775 +from vyos.utils.file import write_file from vyos.utils.dict import dict_search from vyos.utils.network import is_subnet_connected from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = '/run/dhcp-server/dhcpdv6.conf' +config_file = '/run/kea/kea-dhcp6.conf' +ctrl_socket = '/run/kea/dhcp6-ctrl-socket' +lease_file = '/config/dhcp6.leases' def get_config(config=None): if config: @@ -110,17 +115,20 @@ def verify(dhcpv6): # Prefix delegation sanity checks if 'prefix_delegation' in subnet_config: - if 'start' not in subnet_config['prefix_delegation']: - raise ConfigError('prefix-delegation start address not defined!') + if 'prefix' not in subnet_config['prefix_delegation']: + raise ConfigError('prefix-delegation prefix not defined!') - for prefix, prefix_config in subnet_config['prefix_delegation']['start'].items(): - if 'stop' not in prefix_config: - raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}" '\ + for prefix, prefix_config in subnet_config['prefix_delegation']['prefix'].items(): + if 'delegated_length' not in prefix_config: + raise ConfigError(f'Delegated IPv6 prefix length for "{prefix}" '\ f'must be configured') if 'prefix_length' not in prefix_config: raise ConfigError('Length of delegated IPv6 prefix must be configured') + if prefix_config['prefix_length'] > prefix_config['delegated_length']: + raise ConfigError('Length of delegated IPv6 prefix must be within parent prefix') + # Static mappings don't require anything (but check if IP is in subnet if it's set) if 'static_mapping' in subnet_config: for mapping, mapping_config in subnet_config['static_mapping'].items(): @@ -168,12 +176,18 @@ def generate(dhcpv6): if not dhcpv6 or 'disable' in dhcpv6: return None - render(config_file, 'dhcp-server/dhcpdv6.conf.j2', dhcpv6) + dhcpv6['lease_file'] = lease_file + dhcpv6['machine'] = os.uname().machine + + if not os.path.exists(lease_file): + write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755) + + render(config_file, 'dhcp-server/kea-dhcp6.conf.j2', dhcpv6) return None def apply(dhcpv6): # bail out early - looks like removal from running config - service_name = 'isc-dhcp-server6.service' + service_name = 'kea-dhcp6-server.service' if not dhcpv6 or 'disable' in dhcpv6: # DHCP server is removed in the commit call(f'systemctl stop {service_name}') @@ -182,6 +196,16 @@ def apply(dhcpv6): return None call(f'systemctl restart {service_name}') + + # op-mode needs ctrl socket permission change + i = 0 + while not os.path.exists(ctrl_socket): + if i > 15: + break + i += 1 + sleep(1) + chmod_775(ctrl_socket) + return None if __name__ == '__main__': diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 3ddc8e7fd..809c650d9 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -18,6 +18,7 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configverify import verify_interface_exists from vyos.template import render @@ -29,6 +30,9 @@ airbag.enable() config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' +# Dynamic interfaces that might not exist when the configuration is loaded +dynamic_interfaces = ('pppoe', 'sstpc') + # Protocols that require zone zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', 'nfsn', 'nsupdate'] @@ -85,12 +89,19 @@ def verify(dyndns): if field not in config: raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') - # If dyndns address is an interface, ensure that it exists + # If dyndns address is an interface, ensure + # that the interface exists (or just warn if dynamic interface) # and that web-options are not set if config['address'] != 'web': - verify_interface_exists(config['address']) + # exclude check interface for dynamic interfaces + if config['address'].startswith(dynamic_interfaces): + Warning(f'Interface "{config["address"]}" does not exist yet and cannot ' + f'be used for Dynamic DNS service "{service}" until it is up!') + else: + verify_interface_exists(config['address']) if 'web_options' in config: - raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') + raise ConfigError(f'"web-options" is applicable only when using HTTP(S) ' + f'web request to obtain the IP address') # RFC2136 uses 'key' instead of 'password' if config['protocol'] != 'nsupdate' and 'password' not in config: @@ -118,13 +129,16 @@ def verify(dyndns): if config['ip_version'] == 'both': if config['protocol'] not in dualstack_supported: - raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} with protocol "{config["protocol"]}"') + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} ' + f'with protocol "{config["protocol"]}"') # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org) if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers: - raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}" with protocol "{config["protocol"]}"') + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} ' + f'for "{config["server"]}" with protocol "{config["protocol"]}"') if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']): - raise ConfigError(f'"expiry-time" must be greater than "wait-time" for Dynamic DNS service "{service}"') + raise ConfigError(f'"expiry-time" must be greater than "wait-time" for ' + f'Dynamic DNS service "{service}"') return None diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 00015023c..557f0a9e9 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -93,6 +93,7 @@ def get_config(config=None): tmp = conf.get_config_dict(['policy']) # Merge policy dict into "regular" config dict bgp = dict_merge(tmp, bgp) + return bgp diff --git a/src/conf_mode/protocols_segment_routing.py b/src/conf_mode/protocols_segment_routing.py new file mode 100755 index 000000000..eb1653212 --- /dev/null +++ b/src/conf_mode/protocols_segment_routing.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 + +from sys import exit + +from vyos.config import Config +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['protocols', 'segment-routing'] + sr = 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. + sr = conf.merge_defaults(sr, recursive=True) + + return sr + +def verify(static): + return None + +def generate(static): + if not static: + return None + + static['new_frr_config'] = render_to_string('frr/zebra.segment_routing.frr.j2', static) + return None + +def apply(static): + zebra_daemon = 'zebra' + + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(r'^segment-routing') + if 'new_frr_config' in static: + frr_cfg.add_before(frr.default_add_before, static['new_frr_config']) + frr_cfg.commit_configuration(zebra_daemon) + + return None + +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/system-login.py b/src/conf_mode/system-login.py index 87a269499..aeac82462 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -306,6 +306,7 @@ def generate(login): def apply(login): + enable_otp = False if 'user' in login: for user, user_config in login['user'].items(): # make new user using vyatta shell and make home directory (-m), @@ -330,7 +331,7 @@ def apply(login): if tmp: command += f" --home '{tmp}'" else: command += f" --home '/home/{user}'" - command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk {user}' + command += f' --groups frr,frrvty,vyattacfg,sudo,adm,dip,disk,_kea {user}' try: cmd(command) @@ -350,6 +351,7 @@ def apply(login): # Generate 2FA/MFA One-Time-Pad configuration if dict_search('authentication.otp.key', user_config): + enable_otp = True render(f'{home_dir}/.google_authenticator', 'login/pam_otp_ga.conf.j2', user_config, permission=0o400, user=user, group='users') else: @@ -398,6 +400,11 @@ def apply(login): pam_profile = 'tacplus-optional' cmd(f'pam-auth-update --enable {pam_profile}') + # Enable/disable Google authenticator + cmd('pam-auth-update --disable mfa-google-authenticator') + if enable_otp: + cmd(f'pam-auth-update --enable mfa-google-authenticator') + return None diff --git a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf new file mode 100644 index 000000000..0f5bf801e --- /dev/null +++ b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf @@ -0,0 +1,9 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-ctrl-agent -c /run/kea/kea-ctrl-agent.conf +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE diff --git a/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf new file mode 100644 index 000000000..682e5bbce --- /dev/null +++ b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-dhcp4 -c /run/kea/kea-dhcp4.conf diff --git a/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf new file mode 100644 index 000000000..cb33fc057 --- /dev/null +++ b/src/etc/systemd/system/kea-dhcp6-server.service.d/override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/kea-dhcp6 -c /run/kea/kea-dhcp6.conf diff --git a/src/helpers/simple-download.py b/src/helpers/simple-download.py new file mode 100755 index 000000000..501af75f5 --- /dev/null +++ b/src/helpers/simple-download.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import sys +from argparse import ArgumentParser +from vyos.remote import download + +parser = ArgumentParser() +parser.add_argument('--local-file', help='local file', required=True) +parser.add_argument('--remote-path', help='remote path', required=True) + +args = parser.parse_args() + +try: + download(args.local_file, args.remote_path, + check_space=True, raise_error=True) +except Exception as e: + print(e) + sys.exit(1) + +sys.exit() diff --git a/src/init/vyos-router b/src/init/vyos-router index 35095afe4..711681a8e 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -260,6 +260,8 @@ EOF rm -f /etc/pam_radius_auth.conf pam-auth-update --disable tacplus-mandatory tacplus-optional rm -f /etc/tacplus_nss.conf /etc/tacplus_servers + # and no Google authenticator for 2FA/MFA + pam-auth-update --disable mfa-google-authenticator # Certain configuration files are re-generated by the configuration # subsystem and must reside under /etc and can not easily be moved to /run. diff --git a/src/migration-scripts/dhcp-server/6-to-7 b/src/migration-scripts/dhcp-server/6-to-7 new file mode 100755 index 000000000..ccf385a30 --- /dev/null +++ b/src/migration-scripts/dhcp-server/6-to-7 @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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/>. + +# T3316: Migrate to Kea +# - global-parameters will not function +# - shared-network-parameters will not function +# - subnet-parameters will not function +# - static-mapping-parameters will not function +# - host-decl-name is on by default, option removed +# - ping-check no longer supported +# - failover is default enabled on all subnets that exist on failover servers + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'dhcp-server'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + sys.exit(0) + +if config.exists(base + ['host-decl-name']): + config.delete(base + ['host-decl-name']) + +if config.exists(base + ['global-parameters']): + config.delete(base + ['global-parameters']) + +if config.exists(base + ['shared-network-name']): + for network in config.list_nodes(base + ['shared-network-name']): + base_network = base + ['shared-network-name', network] + + if config.exists(base_network + ['ping-check']): + config.delete(base_network + ['ping-check']) + + if config.exists(base_network + ['shared-network-parameters']): + config.delete(base_network +['shared-network-parameters']) + + if not config.exists(base_network + ['subnet']): + continue + + # Run this for every specified 'subnet' + for subnet in config.list_nodes(base_network + ['subnet']): + base_subnet = base_network + ['subnet', subnet] + + if config.exists(base_subnet + ['enable-failover']): + config.delete(base_subnet + ['enable-failover']) + + if config.exists(base_subnet + ['ping-check']): + config.delete(base_subnet + ['ping-check']) + + if config.exists(base_subnet + ['subnet-parameters']): + config.delete(base_subnet + ['subnet-parameters']) + + if config.exists(base_subnet + ['static-mapping']): + for mapping in config.list_nodes(base_subnet + ['static-mapping']): + if config.exists(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']): + config.delete(base_subnet + ['static-mapping', mapping, 'static-mapping-parameters']) + +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/dhcpv6-server/1-to-2 b/src/migration-scripts/dhcpv6-server/1-to-2 new file mode 100755 index 000000000..cc5a8900a --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/1-to-2 @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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/>. + +# T3316: Migrate to Kea +# - Kea was meant to have support for key "prefix-highest" under PD which would allow an address range +# However this seems to have never been implemented. A conversion to prefix length is needed (where possible). +# Ref: https://lists.isc.org/pipermail/kea-users/2022-November/003686.html +# - Remove prefix temporary value, convert to multi leafNode (https://kea.readthedocs.io/en/kea-2.2.0/arm/dhcp6-srv.html#dhcpv6-server-limitations) + +import sys +from vyos.configtree import ConfigTree +from vyos.utils.network import ipv6_prefix_length + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'dhcpv6-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +for network in config.list_nodes(base): + if not config.exists(base + [network, 'subnet']): + continue + + for subnet in config.list_nodes(base + [network, 'subnet']): + # Delete temporary value under address-range prefix, convert tagNode to leafNode multi + if config.exists(base + [network, 'subnet', subnet, 'address-range', 'prefix']): + prefix_base = base + [network, 'subnet', subnet, 'address-range', 'prefix'] + prefixes = config.list_nodes(prefix_base) + + config.delete(prefix_base) + + for prefix in prefixes: + config.set(prefix_base, value=prefix, replace=False) + + if config.exists(base + [network, 'subnet', subnet, 'prefix-delegation', 'prefix']): + prefix_base = base + [network, 'subnet', subnet, 'prefix-delegation', 'prefix'] + + config.set(prefix_base) + config.set_tag(prefix_base) + + for start in config.list_nodes(base + [network, 'subnet', subnet, 'prefix-delegation', 'start']): + path = base + [network, 'subnet', subnet, 'prefix-delegation', 'start', start] + + delegated_length = config.return_value(path + ['prefix-length']) + stop = config.return_value(path + ['stop']) + + prefix_length = ipv6_prefix_length(start, stop) + + # This range could not be converted into a simple prefix length and must be skipped + if not prefix_length: + continue + + config.set(prefix_base + [start, 'delegated-length'], value=delegated_length) + config.set(prefix_base + [start, 'prefix-length'], value=prefix_length) + + config.delete(base + [network, 'subnet', subnet, 'prefix-delegation', 'start']) + +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/interfaces/22-to-23 b/src/migration-scripts/interfaces/22-to-23 index 8b21fce51..04e023e77 100755 --- a/src/migration-scripts/interfaces/22-to-23 +++ b/src/migration-scripts/interfaces/22-to-23 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 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 @@ -13,133 +13,45 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - -from sys import argv -from sys import exit +# +# Deletes Wireguard peers if they have the same public key as the router has. +import sys from vyos.configtree import ConfigTree - -def migrate_ospf(config, path, interface): - path = path + ['ospf'] - if config.exists(path): - new_base = ['protocols', 'ospf', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ip ospf" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_ospfv3(config, path, interface): - path = path + ['ospfv3'] - if config.exists(path): - new_base = ['protocols', 'ospfv3', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ipv6 ospfv3" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_rip(config, path, interface): - path = path + ['rip'] - if config.exists(path): - new_base = ['protocols', 'rip', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ip rip" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_ripng(config, path, interface): - path = path + ['ripng'] - if config.exists(path): - new_base = ['protocols', 'ripng', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ipv6 ripng" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) +from vyos.utils.network import is_wireguard_key_pair if __name__ == '__main__': - if len(argv) < 2: + if len(sys.argv) < 2: print("Must specify file name!") - exit(1) + sys.exit(1) + + file_name = sys.argv[1] - file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) - - # - # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" - # - for type in config.list_nodes(['interfaces']): - for interface in config.list_nodes(['interfaces', type]): - ip_base = ['interfaces', type, interface, 'ip'] - ipv6_base = ['interfaces', type, interface, 'ipv6'] - migrate_rip(config, ip_base, interface) - migrate_ripng(config, ipv6_base, interface) - migrate_ospf(config, ip_base, interface) - migrate_ospfv3(config, ipv6_base, interface) - - vif_path = ['interfaces', type, interface, 'vif'] - if config.exists(vif_path): - for vif in config.list_nodes(vif_path): - vif_ip_base = vif_path + [vif, 'ip'] - vif_ipv6_base = vif_path + [vif, 'ipv6'] - ifname = f'{interface}.{vif}' - - migrate_rip(config, vif_ip_base, ifname) - migrate_ripng(config, vif_ipv6_base, ifname) - migrate_ospf(config, vif_ip_base, ifname) - migrate_ospfv3(config, vif_ipv6_base, ifname) - - - vif_s_path = ['interfaces', type, interface, 'vif-s'] - if config.exists(vif_s_path): - for vif_s in config.list_nodes(vif_s_path): - vif_s_ip_base = vif_s_path + [vif_s, 'ip'] - vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] - - # vif-c interfaces MUST be migrated before their parent vif-s - # interface as the migrate_*() functions delete the path! - vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] - if config.exists(vif_c_path): - for vif_c in config.list_nodes(vif_c_path): - vif_c_ip_base = vif_c_path + [vif_c, 'ip'] - vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] - ifname = f'{interface}.{vif_s}.{vif_c}' - - migrate_rip(config, vif_c_ip_base, ifname) - migrate_ripng(config, vif_c_ipv6_base, ifname) - migrate_ospf(config, vif_c_ip_base, ifname) - migrate_ospfv3(config, vif_c_ipv6_base, ifname) - - - ifname = f'{interface}.{vif_s}' - migrate_rip(config, vif_s_ip_base, ifname) - migrate_ripng(config, vif_s_ipv6_base, ifname) - migrate_ospf(config, vif_s_ip_base, ifname) - migrate_ospfv3(config, vif_s_ipv6_base, ifname) + base = ['interfaces', 'wireguard'] + if not config.exists(base): + # Nothing to do + sys.exit(0) + for interface in config.list_nodes(base): + if not config.exists(base + [interface, 'private-key']): + continue + private_key = config.return_value(base + [interface, 'private-key']) + interface_base = base + [interface] + if config.exists(interface_base + ['peer']): + for peer in config.list_nodes(interface_base + ['peer']): + peer_base = interface_base + ['peer', peer] + if not config.exists(peer_base + ['public-key']): + continue + peer_public_key = config.return_value(peer_base + ['public-key']) + if not config.exists(peer_base + ['disable']) \ + and is_wireguard_key_pair(private_key, peer_public_key): + config.set(peer_base + ['disable']) 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) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/23-to-24 b/src/migration-scripts/interfaces/23-to-24 index 8fd79ecc6..8b21fce51 100755 --- a/src/migration-scripts/interfaces/23-to-24 +++ b/src/migration-scripts/interfaces/23-to-24 @@ -14,47 +14,132 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported -# having a VTI interface in the CLI but no IPSec configuration - drop VTI -# configuration if this is the case for VyOS 1.4 - -import sys +from sys import argv +from sys import exit from vyos.configtree import ConfigTree +def migrate_ospf(config, path, interface): + path = path + ['ospf'] + if config.exists(path): + new_base = ['protocols', 'ospf', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ip ospf" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_ospfv3(config, path, interface): + path = path + ['ospfv3'] + if config.exists(path): + new_base = ['protocols', 'ospfv3', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ipv6 ospfv3" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_rip(config, path, interface): + path = path + ['rip'] + if config.exists(path): + new_base = ['protocols', 'rip', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ip rip" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_ripng(config, path, interface): + path = path + ['ripng'] + if config.exists(path): + new_base = ['protocols', 'ripng', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ipv6 ripng" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + if __name__ == '__main__': - if len(sys.argv) < 2: + if len(argv) < 2: print("Must specify file name!") - sys.exit(1) - - file_name = sys.argv[1] + exit(1) + file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) - base = ['interfaces', 'vti'] - if not config.exists(base): - # Nothing to do - sys.exit(0) - - ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] - for interface in config.list_nodes(base): - found = False - if config.exists(ipsec_base): - for peer in config.list_nodes(ipsec_base): - if config.exists(ipsec_base + [peer, 'vti', 'bind']): - tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind']) - if tmp == interface: - # Interface was found and we no longer need to search - # for it in our IPSec peers - found = True - break - if not found: - config.delete(base + [interface]) + + # + # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" + # + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + ip_base = ['interfaces', type, interface, 'ip'] + ipv6_base = ['interfaces', type, interface, 'ipv6'] + migrate_rip(config, ip_base, interface) + migrate_ripng(config, ipv6_base, interface) + migrate_ospf(config, ip_base, interface) + migrate_ospfv3(config, ipv6_base, interface) + + vif_path = ['interfaces', type, interface, 'vif'] + if config.exists(vif_path): + for vif in config.list_nodes(vif_path): + vif_ip_base = vif_path + [vif, 'ip'] + vif_ipv6_base = vif_path + [vif, 'ipv6'] + ifname = f'{interface}.{vif}' + + migrate_rip(config, vif_ip_base, ifname) + migrate_ripng(config, vif_ipv6_base, ifname) + migrate_ospf(config, vif_ip_base, ifname) + migrate_ospfv3(config, vif_ipv6_base, ifname) + + + vif_s_path = ['interfaces', type, interface, 'vif-s'] + if config.exists(vif_s_path): + for vif_s in config.list_nodes(vif_s_path): + vif_s_ip_base = vif_s_path + [vif_s, 'ip'] + vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] + + # vif-c interfaces MUST be migrated before their parent vif-s + # interface as the migrate_*() functions delete the path! + vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] + if config.exists(vif_c_path): + for vif_c in config.list_nodes(vif_c_path): + vif_c_ip_base = vif_c_path + [vif_c, 'ip'] + vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] + ifname = f'{interface}.{vif_s}.{vif_c}' + + migrate_rip(config, vif_c_ip_base, ifname) + migrate_ripng(config, vif_c_ipv6_base, ifname) + migrate_ospf(config, vif_c_ip_base, ifname) + migrate_ospfv3(config, vif_c_ipv6_base, ifname) + + + ifname = f'{interface}.{vif_s}' + migrate_rip(config, vif_s_ip_base, ifname) + migrate_ripng(config, vif_s_ipv6_base, ifname) + migrate_ospf(config, vif_s_ip_base, ifname) + migrate_ospfv3(config, vif_s_ipv6_base, ifname) 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)) - sys.exit(1) + exit(1) diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25 index 9aa6ea5e3..8fd79ecc6 100755 --- a/src/migration-scripts/interfaces/24-to-25 +++ b/src/migration-scripts/interfaces/24-to-25 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# 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 @@ -14,374 +14,47 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# Migrate Wireguard to store keys in CLI -# Migrate EAPoL to PKI configuration +# A VTI interface also requires an IPSec configuration - VyOS 1.2 supported +# having a VTI interface in the CLI but no IPSec configuration - drop VTI +# configuration if this is the case for VyOS 1.4 -import os import sys - from vyos.configtree import ConfigTree -from vyos.pki import CERT_BEGIN -from vyos.pki import load_certificate -from vyos.pki import load_crl -from vyos.pki import load_dh_parameters -from vyos.pki import load_private_key -from vyos.pki import encode_certificate -from vyos.pki import encode_dh_parameters -from vyos.pki import encode_private_key -from vyos.pki import verify_crl -from vyos.utils.process import run - -def wrapped_pem_to_config_value(pem): - out = [] - for line in pem.strip().split("\n"): - if not line or line.startswith("-----") or line[0] == '#': - continue - out.append(line) - return "".join(out) - -def read_file_for_pki(config_auth_path): - full_path = os.path.join(AUTH_DIR, config_auth_path) - output = None - - if os.path.isfile(full_path): - if not os.access(full_path, os.R_OK): - run(f'sudo chmod 644 {full_path}') - - with open(full_path, 'r') as f: - output = f.read() - - return output -if len(sys.argv) < 2: - print("Must specify file name!") - sys.exit(1) +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) -file_name = sys.argv[1] + file_name = sys.argv[1] -with open(file_name, 'r') as f: - config_file = f.read() + with open(file_name, 'r') as f: + config_file = f.read() -config = ConfigTree(config_file) + config = ConfigTree(config_file) + base = ['interfaces', 'vti'] + if not config.exists(base): + # Nothing to do + sys.exit(0) -AUTH_DIR = '/config/auth' -pki_base = ['pki'] - -# OpenVPN -base = ['interfaces', 'openvpn'] - -if config.exists(base): + ipsec_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] for interface in config.list_nodes(base): - x509_base = base + [interface, 'tls'] - pki_name = f'openvpn_{interface}' - - if config.exists(base + [interface, 'shared-secret-key-file']): - if not config.exists(pki_base + ['openvpn', 'shared-secret']): - config.set(pki_base + ['openvpn', 'shared-secret']) - config.set_tag(pki_base + ['openvpn', 'shared-secret']) - - key_file = config.return_value(base + [interface, 'shared-secret-key-file']) - key = read_file_for_pki(key_file) - key_pki_name = f'{pki_name}_shared' - - if key: - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') - config.set(base + [interface, 'shared-secret-key'], value=key_pki_name) - else: - print(f'Failed to migrate shared-secret-key on openvpn interface {interface}') - - config.delete(base + [interface, 'shared-secret-key-file']) - - if not config.exists(base + [interface, 'tls']): - continue - - if config.exists(base + [interface, 'tls', 'auth-file']): - if not config.exists(pki_base + ['openvpn', 'shared-secret']): - config.set(pki_base + ['openvpn', 'shared-secret']) - config.set_tag(pki_base + ['openvpn', 'shared-secret']) - - key_file = config.return_value(base + [interface, 'tls', 'auth-file']) - key = read_file_for_pki(key_file) - key_pki_name = f'{pki_name}_auth' - - if key: - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') - config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name) - else: - print(f'Failed to migrate auth-key on openvpn interface {interface}') - - config.delete(base + [interface, 'tls', 'auth-file']) - - if config.exists(base + [interface, 'tls', 'crypt-file']): - if not config.exists(pki_base + ['openvpn', 'shared-secret']): - config.set(pki_base + ['openvpn', 'shared-secret']) - config.set_tag(pki_base + ['openvpn', 'shared-secret']) - - key_file = config.return_value(base + [interface, 'tls', 'crypt-file']) - key = read_file_for_pki(key_file) - key_pki_name = f'{pki_name}_crypt' - - if key: - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) - config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') - config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name) - else: - print(f'Failed to migrate crypt-key on openvpn interface {interface}') - - config.delete(base + [interface, 'tls', 'crypt-file']) - - ca_certs = {} - - if config.exists(x509_base + ['ca-cert-file']): - if not config.exists(pki_base + ['ca']): - config.set(pki_base + ['ca']) - config.set_tag(pki_base + ['ca']) - - cert_file = config.return_value(x509_base + ['ca-cert-file']) - cert_path = os.path.join(AUTH_DIR, cert_file) - - if os.path.isfile(cert_path): - if not os.access(cert_path, os.R_OK): - run(f'sudo chmod 644 {cert_path}') - - with open(cert_path, 'r') as f: - certs_str = f.read() - certs_data = certs_str.split(CERT_BEGIN) - index = 1 - for cert_data in certs_data[1:]: - cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) - - if cert: - ca_certs[f'{pki_name}_{index}'] = cert - cert_pem = encode_certificate(cert) - config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) - else: - print(f'Failed to migrate CA certificate on openvpn interface {interface}') - - index += 1 - else: - print(f'Failed to migrate CA certificate on openvpn interface {interface}') - - config.delete(x509_base + ['ca-cert-file']) - - if config.exists(x509_base + ['crl-file']): - if not config.exists(pki_base + ['ca']): - config.set(pki_base + ['ca']) - config.set_tag(pki_base + ['ca']) - - crl_file = config.return_value(x509_base + ['crl-file']) - crl_path = os.path.join(AUTH_DIR, crl_file) - crl = None - crl_ca_name = None - - if os.path.isfile(crl_path): - if not os.access(crl_path, os.R_OK): - run(f'sudo chmod 644 {crl_path}') - - with open(crl_path, 'r') as f: - crl_data = f.read() - crl = load_crl(crl_data, wrap_tags=False) - - for ca_name, ca_cert in ca_certs.items(): - if verify_crl(crl, ca_cert): - crl_ca_name = ca_name - break - - if crl and crl_ca_name: - crl_pem = encode_certificate(crl) - config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) - else: - print(f'Failed to migrate CRL on openvpn interface {interface}') - - config.delete(x509_base + ['crl-file']) - - if config.exists(x509_base + ['cert-file']): - if not config.exists(pki_base + ['certificate']): - config.set(pki_base + ['certificate']) - config.set_tag(pki_base + ['certificate']) - - cert_file = config.return_value(x509_base + ['cert-file']) - cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None - - if os.path.isfile(cert_path): - if not os.access(cert_path, os.R_OK): - run(f'sudo chmod 644 {cert_path}') - - with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['certificate'], value=pki_name) - else: - print(f'Failed to migrate certificate on openvpn interface {interface}') - - config.delete(x509_base + ['cert-file']) - - if config.exists(x509_base + ['key-file']): - key_file = config.return_value(x509_base + ['key-file']) - key_path = os.path.join(AUTH_DIR, key_file) - key = None - - if os.path.isfile(key_path): - if not os.access(key_path, os.R_OK): - run(f'sudo chmod 644 {key_path}') - - with open(key_path, 'r') as f: - key_data = f.read() - key = load_private_key(key_data, passphrase=None, wrap_tags=False) - - if key: - key_pem = encode_private_key(key, passphrase=None) - config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) - else: - print(f'Failed to migrate private key on openvpn interface {interface}') - - config.delete(x509_base + ['key-file']) - - if config.exists(x509_base + ['dh-file']): - if not config.exists(pki_base + ['dh']): - config.set(pki_base + ['dh']) - config.set_tag(pki_base + ['dh']) - - dh_file = config.return_value(x509_base + ['dh-file']) - dh_path = os.path.join(AUTH_DIR, dh_file) - dh = None - - if os.path.isfile(dh_path): - if not os.access(dh_path, os.R_OK): - run(f'sudo chmod 644 {dh_path}') - - with open(dh_path, 'r') as f: - dh_data = f.read() - dh = load_dh_parameters(dh_data, wrap_tags=False) - - if dh: - dh_pem = encode_dh_parameters(dh) - config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem)) - config.set(x509_base + ['dh-params'], value=pki_name) - else: - print(f'Failed to migrate DH parameters on openvpn interface {interface}') - - config.delete(x509_base + ['dh-file']) - -# Wireguard -base = ['interfaces', 'wireguard'] - -if config.exists(base): - for interface in config.list_nodes(base): - private_key_path = base + [interface, 'private-key'] - - key_file = 'default' - if config.exists(private_key_path): - key_file = config.return_value(private_key_path) - - full_key_path = f'/config/auth/wireguard/{key_file}/private.key' - - if not os.path.exists(full_key_path): - print(f'Could not find wireguard private key for migration on interface "{interface}"') - continue - - with open(full_key_path, 'r') as f: - key_data = f.read().strip() - config.set(private_key_path, value=key_data) - - for peer in config.list_nodes(base + [interface, 'peer']): - config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key') - -# Ethernet EAPoL -base = ['interfaces', 'ethernet'] - -if config.exists(base): - for interface in config.list_nodes(base): - if not config.exists(base + [interface, 'eapol']): - continue - - x509_base = base + [interface, 'eapol'] - pki_name = f'eapol_{interface}' - - if config.exists(x509_base + ['ca-cert-file']): - if not config.exists(pki_base + ['ca']): - config.set(pki_base + ['ca']) - config.set_tag(pki_base + ['ca']) - - cert_file = config.return_value(x509_base + ['ca-cert-file']) - cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None - - if os.path.isfile(cert_path): - if not os.access(cert_path, os.R_OK): - run(f'sudo chmod 644 {cert_path}') - - with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['ca-certificate'], value=pki_name) - else: - print(f'Failed to migrate CA certificate on eapol config for interface {interface}') - - config.delete(x509_base + ['ca-cert-file']) - - if config.exists(x509_base + ['cert-file']): - if not config.exists(pki_base + ['certificate']): - config.set(pki_base + ['certificate']) - config.set_tag(pki_base + ['certificate']) - - cert_file = config.return_value(x509_base + ['cert-file']) - cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None - - if os.path.isfile(cert_path): - if not os.access(cert_path, os.R_OK): - run(f'sudo chmod 644 {cert_path}') - - with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['certificate'], value=pki_name) - else: - print(f'Failed to migrate certificate on eapol config for interface {interface}') - - config.delete(x509_base + ['cert-file']) - - if config.exists(x509_base + ['key-file']): - key_file = config.return_value(x509_base + ['key-file']) - key_path = os.path.join(AUTH_DIR, key_file) - key = None - - if os.path.isfile(key_path): - if not os.access(key_path, os.R_OK): - run(f'sudo chmod 644 {key_path}') - - with open(key_path, 'r') as f: - key_data = f.read() - key = load_private_key(key_data, passphrase=None, wrap_tags=False) - - if key: - key_pem = encode_private_key(key, passphrase=None) - config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) - else: - print(f'Failed to migrate private key on eapol config for interface {interface}') - - config.delete(x509_base + ['key-file']) - -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)) - sys.exit(1) + found = False + if config.exists(ipsec_base): + for peer in config.list_nodes(ipsec_base): + if config.exists(ipsec_base + [peer, 'vti', 'bind']): + tmp = config.return_value(ipsec_base + [peer, 'vti', 'bind']) + if tmp == interface: + # Interface was found and we no longer need to search + # for it in our IPSec peers + found = True + break + if not found: + config.delete(base + [interface]) + + 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)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/25-to-26 b/src/migration-scripts/interfaces/25-to-26 index 4967a29fa..9aa6ea5e3 100755 --- a/src/migration-scripts/interfaces/25-to-26 +++ b/src/migration-scripts/interfaces/25-to-26 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2021-2023 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,41 +14,374 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# T4384: pppoe: replace default-route CLI option with common CLI nodes already -# present for DHCP +# Migrate Wireguard to store keys in CLI +# Migrate EAPoL to PKI configuration -from sys import argv +import os +import sys -from vyos.ethtool import Ethtool from vyos.configtree import ConfigTree +from vyos.pki import CERT_BEGIN +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_dh_parameters +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_dh_parameters +from vyos.pki import encode_private_key +from vyos.pki import verify_crl +from vyos.utils.process import run -if len(argv) < 2: +def wrapped_pem_to_config_value(pem): + out = [] + for line in pem.strip().split("\n"): + if not line or line.startswith("-----") or line[0] == '#': + continue + out.append(line) + return "".join(out) + +def read_file_for_pki(config_auth_path): + full_path = os.path.join(AUTH_DIR, config_auth_path) + output = None + + if os.path.isfile(full_path): + if not os.access(full_path, os.R_OK): + run(f'sudo chmod 644 {full_path}') + + with open(full_path, 'r') as f: + output = f.read() + + return output + +if len(sys.argv) < 2: print("Must specify file name!") - exit(1) + sys.exit(1) + +file_name = sys.argv[1] -file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() -base = ['interfaces', 'pppoe'] config = ConfigTree(config_file) -if not config.exists(base): - exit(0) +AUTH_DIR = '/config/auth' +pki_base = ['pki'] + +# OpenVPN +base = ['interfaces', 'openvpn'] + +if config.exists(base): + for interface in config.list_nodes(base): + x509_base = base + [interface, 'tls'] + pki_name = f'openvpn_{interface}' + + if config.exists(base + [interface, 'shared-secret-key-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'shared-secret-key-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_shared' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'shared-secret-key'], value=key_pki_name) + else: + print(f'Failed to migrate shared-secret-key on openvpn interface {interface}') + + config.delete(base + [interface, 'shared-secret-key-file']) + + if not config.exists(base + [interface, 'tls']): + continue + + if config.exists(base + [interface, 'tls', 'auth-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'tls', 'auth-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_auth' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name) + else: + print(f'Failed to migrate auth-key on openvpn interface {interface}') + + config.delete(base + [interface, 'tls', 'auth-file']) + + if config.exists(base + [interface, 'tls', 'crypt-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'tls', 'crypt-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_crypt' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name) + else: + print(f'Failed to migrate crypt-key on openvpn interface {interface}') + + config.delete(base + [interface, 'tls', 'crypt-file']) + + ca_certs = {} + + if config.exists(x509_base + ['ca-cert-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + certs_str = f.read() + certs_data = certs_str.split(CERT_BEGIN) + index = 1 + for cert_data in certs_data[1:]: + cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) + + if cert: + ca_certs[f'{pki_name}_{index}'] = cert + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) + else: + print(f'Failed to migrate CA certificate on openvpn interface {interface}') + + index += 1 + else: + print(f'Failed to migrate CA certificate on openvpn interface {interface}') + + config.delete(x509_base + ['ca-cert-file']) + + if config.exists(x509_base + ['crl-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + crl_file = config.return_value(x509_base + ['crl-file']) + crl_path = os.path.join(AUTH_DIR, crl_file) + crl = None + crl_ca_name = None + + if os.path.isfile(crl_path): + if not os.access(crl_path, os.R_OK): + run(f'sudo chmod 644 {crl_path}') + + with open(crl_path, 'r') as f: + crl_data = f.read() + crl = load_crl(crl_data, wrap_tags=False) + + for ca_name, ca_cert in ca_certs.items(): + if verify_crl(crl, ca_cert): + crl_ca_name = ca_name + break + + if crl and crl_ca_name: + crl_pem = encode_certificate(crl) + config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + else: + print(f'Failed to migrate CRL on openvpn interface {interface}') + + config.delete(x509_base + ['crl-file']) + + if config.exists(x509_base + ['cert-file']): + if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on openvpn interface {interface}') + + config.delete(x509_base + ['cert-file']) + + if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on openvpn interface {interface}') + + config.delete(x509_base + ['key-file']) + + if config.exists(x509_base + ['dh-file']): + if not config.exists(pki_base + ['dh']): + config.set(pki_base + ['dh']) + config.set_tag(pki_base + ['dh']) + + dh_file = config.return_value(x509_base + ['dh-file']) + dh_path = os.path.join(AUTH_DIR, dh_file) + dh = None + + if os.path.isfile(dh_path): + if not os.access(dh_path, os.R_OK): + run(f'sudo chmod 644 {dh_path}') + + with open(dh_path, 'r') as f: + dh_data = f.read() + dh = load_dh_parameters(dh_data, wrap_tags=False) + + if dh: + dh_pem = encode_dh_parameters(dh) + config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem)) + config.set(x509_base + ['dh-params'], value=pki_name) + else: + print(f'Failed to migrate DH parameters on openvpn interface {interface}') + + config.delete(x509_base + ['dh-file']) + +# Wireguard +base = ['interfaces', 'wireguard'] + +if config.exists(base): + for interface in config.list_nodes(base): + private_key_path = base + [interface, 'private-key'] + + key_file = 'default' + if config.exists(private_key_path): + key_file = config.return_value(private_key_path) + + full_key_path = f'/config/auth/wireguard/{key_file}/private.key' + + if not os.path.exists(full_key_path): + print(f'Could not find wireguard private key for migration on interface "{interface}"') + continue + + with open(full_key_path, 'r') as f: + key_data = f.read().strip() + config.set(private_key_path, value=key_data) + + for peer in config.list_nodes(base + [interface, 'peer']): + config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key') + +# Ethernet EAPoL +base = ['interfaces', 'ethernet'] + +if config.exists(base): + for interface in config.list_nodes(base): + if not config.exists(base + [interface, 'eapol']): + continue + + x509_base = base + [interface, 'eapol'] + pki_name = f'eapol_{interface}' + + if config.exists(x509_base + ['ca-cert-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on eapol config for interface {interface}') + + config.delete(x509_base + ['ca-cert-file']) + + if config.exists(x509_base + ['cert-file']): + if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on eapol config for interface {interface}') + + config.delete(x509_base + ['cert-file']) + + if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on eapol config for interface {interface}') -for ifname in config.list_nodes(base): - tmp_config = base + [ifname, 'default-route'] - if config.exists(tmp_config): - # Retrieve current config value - value = config.return_value(tmp_config) - # Delete old Config node - config.delete(tmp_config) - if value == 'none': - config.set(base + [ifname, 'no-default-route']) + config.delete(x509_base + ['key-file']) 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) + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/26-to-27 b/src/migration-scripts/interfaces/26-to-27 index a0d043d11..4967a29fa 100755 --- a/src/migration-scripts/interfaces/26-to-27 +++ b/src/migration-scripts/interfaces/26-to-27 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 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,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# T4995: pppoe, wwan, sstpc-client rename "authentication user" CLI node -# to "authentication username" +# T4384: pppoe: replace default-route CLI option with common CLI nodes already +# present for DHCP from sys import argv @@ -30,16 +30,21 @@ file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() +base = ['interfaces', 'pppoe'] config = ConfigTree(config_file) -for type in ['pppoe', 'sstpc-client', 'wwam']: - base = ['interfaces', type] - if not config.exists(base): - continue - for interface in config.list_nodes(base): - auth_base = base + [interface, 'authentication', 'user'] - if config.exists(auth_base): - config.rename(auth_base, 'username') +if not config.exists(base): + exit(0) + +for ifname in config.list_nodes(base): + tmp_config = base + [ifname, 'default-route'] + if config.exists(tmp_config): + # Retrieve current config value + value = config.return_value(tmp_config) + # Delete old Config node + config.delete(tmp_config) + if value == 'none': + config.set(base + [ifname, 'no-default-route']) try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/27-to-28 b/src/migration-scripts/interfaces/27-to-28 index ad5bfa653..a0d043d11 100755 --- a/src/migration-scripts/interfaces/27-to-28 +++ b/src/migration-scripts/interfaces/27-to-28 @@ -14,8 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# T5034: tunnel: rename "multicast enable" CLI node to "enable-multicast" -# valueless node. +# T4995: pppoe, wwan, sstpc-client rename "authentication user" CLI node +# to "authentication username" from sys import argv @@ -30,21 +30,16 @@ file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() -base = ['interfaces', 'tunnel'] config = ConfigTree(config_file) -if not config.exists(base): - exit(0) - -for ifname in config.list_nodes(base): - multicast_base = base + [ifname, 'multicast'] - if config.exists(multicast_base): - tmp = config.return_value(multicast_base) - print(tmp) - # Delete old Config node - config.delete(multicast_base) - if tmp == 'enable': - config.set(base + [ifname, 'enable-multicast']) +for type in ['pppoe', 'sstpc-client', 'wwam']: + base = ['interfaces', type] + if not config.exists(base): + continue + for interface in config.list_nodes(base): + auth_base = base + [interface, 'authentication', 'user'] + if config.exists(auth_base): + config.rename(auth_base, 'username') try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/28-to-29 b/src/migration-scripts/interfaces/28-to-29 index acb6ee1fb..ad5bfa653 100755 --- a/src/migration-scripts/interfaces/28-to-29 +++ b/src/migration-scripts/interfaces/28-to-29 @@ -14,7 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# T5286: remove XDP support in favour of VPP +# T5034: tunnel: rename "multicast enable" CLI node to "enable-multicast" +# valueless node. from sys import argv @@ -29,17 +30,21 @@ file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() -supports_xdp = ['bonding', 'ethernet'] +base = ['interfaces', 'tunnel'] config = ConfigTree(config_file) -for if_type in supports_xdp: - base = ['interfaces', if_type] - if not config.exists(base): - continue - for interface in config.list_nodes(base): - if_base = base + [interface] - if config.exists(if_base + ['xdp']): - config.delete(if_base + ['xdp']) +if not config.exists(base): + exit(0) + +for ifname in config.list_nodes(base): + multicast_base = base + [ifname, 'multicast'] + if config.exists(multicast_base): + tmp = config.return_value(multicast_base) + print(tmp) + # Delete old Config node + config.delete(multicast_base) + if tmp == 'enable': + config.set(base + [ifname, 'enable-multicast']) try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/29-to-30 b/src/migration-scripts/interfaces/29-to-30 index 97e1b329c..acb6ee1fb 100755 --- a/src/migration-scripts/interfaces/29-to-30 +++ b/src/migration-scripts/interfaces/29-to-30 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2023 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 @@ -13,42 +13,37 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Deletes Wireguard peers if they have the same public key as the router has. -import sys + +# T5286: remove XDP support in favour of VPP + +from sys import argv + +from vyos.ethtool import Ethtool from vyos.configtree import ConfigTree -from vyos.utils.network import is_wireguard_key_pair -if __name__ == '__main__': - if len(sys.argv) < 2: - print("Must specify file name!") - sys.exit(1) +if len(argv) < 2: + print("Must specify file name!") + exit(1) - file_name = sys.argv[1] +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() - with open(file_name, 'r') as f: - config_file = f.read() +supports_xdp = ['bonding', 'ethernet'] +config = ConfigTree(config_file) - config = ConfigTree(config_file) - base = ['interfaces', 'wireguard'] +for if_type in supports_xdp: + base = ['interfaces', if_type] if not config.exists(base): - # Nothing to do - sys.exit(0) + continue for interface in config.list_nodes(base): - private_key = config.return_value(base + [interface, 'private-key']) - interface_base = base + [interface] - if config.exists(interface_base + ['peer']): - for peer in config.list_nodes(interface_base + ['peer']): - peer_base = interface_base + ['peer', peer] - peer_public_key = config.return_value(peer_base + ['public-key']) - if config.exists(peer_base + ['public-key']): - if not config.exists(peer_base + ['disable']) \ - and is_wireguard_key_pair(private_key, peer_public_key): - config.set(peer_base + ['disable']) - - 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)) - sys.exit(1) + if_base = base + [interface] + if config.exists(if_base + ['xdp']): + config.delete(if_base + ['xdp']) + +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/nat/6-to-7 b/src/migration-scripts/nat/6-to-7 index b5f6328ef..a2e735394 100755 --- a/src/migration-scripts/nat/6-to-7 +++ b/src/migration-scripts/nat/6-to-7 @@ -21,6 +21,7 @@ # to # 'set nat [source|destination] rule X [inbound-interface|outbound interface] name <iface>' # 'set nat [source|destination] rule X [inbound-interface|outbound interface] group <iface_group>' +# Also remove command if interface == any from sys import argv,exit from vyos.configtree import ConfigTree @@ -56,8 +57,11 @@ for direction in ['source', 'destination']: if config.exists(base + [iface]): if config.exists(base + [iface, 'interface-name']): tmp = config.return_value(base + [iface, 'interface-name']) - config.delete(base + [iface, 'interface-name']) - config.set(base + [iface, 'name'], value=tmp) + if tmp != 'any': + config.delete(base + [iface, 'interface-name']) + config.set(base + [iface, 'name'], value=tmp) + else: + config.delete(base + [iface]) try: with open(file_name, 'w') as f: diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py index f372d3af0..2c95a2b08 100755 --- a/src/op_mode/clear_dhcp_lease.py +++ b/src/op_mode/clear_dhcp_lease.py @@ -1,20 +1,34 @@ #!/usr/bin/env python3 +# +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. import argparse import re -from isc_dhcp_leases import Lease -from isc_dhcp_leases import IscDhcpLeases - from vyos.configquery import ConfigTreeQuery +from vyos.kea import kea_parse_leases from vyos.utils.io import ask_yes_no from vyos.utils.process import call from vyos.utils.commit import commit_in_progress +# TODO: Update to use Kea control socket command "lease4-del" config = ConfigTreeQuery() base = ['service', 'dhcp-server'] -lease_file = '/config/dhcpd.leases' +lease_file = '/config/dhcp4.leases' def del_lease_ip(address): @@ -25,8 +39,7 @@ def del_lease_ip(address): """ with open(lease_file, encoding='utf-8') as f: data = f.read().rstrip() - lease_config_ip = '{(?P<config>[\s\S]+?)\n}' - pattern = rf"lease {address} {lease_config_ip}" + pattern = rf"^{address},[^\n]+\n" # Delete lease for ip block data = re.sub(pattern, '', data) @@ -38,15 +51,13 @@ def is_ip_in_leases(address): """ Return True if address found in the lease file """ - leases = IscDhcpLeases(lease_file) + leases = kea_parse_leases(lease_file) lease_ips = [] - for lease in leases.get(): - lease_ips.append(lease.ip) - if address not in lease_ips: - print(f'Address "{address}" not found in "{lease_file}"') - return False - return True - + for lease in leases: + if address == lease['address']: + return True + print(f'Address "{address}" not found in "{lease_file}"') + return False if not config.exists(base): print('DHCP-server not configured!') @@ -75,4 +86,4 @@ if __name__ == '__main__': exit(1) else: del_lease_ip(address) - call('systemctl restart isc-dhcp-server.service') + call('systemctl restart kea-dhcp4-server.service') diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index d6b8aa0b8..a9271ea79 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -21,7 +21,6 @@ import typing from datetime import datetime from glob import glob from ipaddress import ip_address -from isc_dhcp_leases import IscDhcpLeases from tabulate import tabulate import vyos.opmode @@ -29,6 +28,9 @@ import vyos.opmode from vyos.base import Warning from vyos.configquery import ConfigTreeQuery +from vyos.kea import kea_get_active_config +from vyos.kea import kea_get_pool_from_subnet_id +from vyos.kea import kea_parse_leases from vyos.utils.dict import dict_search from vyos.utils.file import read_file from vyos.utils.process import cmd @@ -77,67 +79,62 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig Get DHCP server leases :return list """ - lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases' + lease_file = '/config/dhcp6.leases' if family == 'inet6' else '/config/dhcp4.leases' data = [] - leases = IscDhcpLeases(lease_file).get() + leases = kea_parse_leases(lease_file) if pool is None: pool = _get_dhcp_pools(family=family) - aux = False else: pool = [pool] - aux = True - - ## Search leases for every pool - for pool_name in pool: - for lease in leases: - if lease.sets.get('shared-networkname', '') == pool_name or lease.sets.get('shared-networkname', '') == '': - #if lease.sets.get('shared-networkname', '') == pool_name: - data_lease = {} - data_lease['ip'] = lease.ip - data_lease['state'] = lease.binding_state - #data_lease['pool'] = pool_name if lease.sets.get('shared-networkname', '') != '' else 'Fail-Over Server' - data_lease['pool'] = lease.sets.get('shared-networkname', '') - data_lease['end'] = lease.end.timestamp() if lease.end else None - data_lease['origin'] = 'local' if data_lease['pool'] != '' else 'remote' - - if family == 'inet': - data_lease['mac'] = lease.ethernet - data_lease['start'] = lease.start.timestamp() - data_lease['hostname'] = lease.hostname - - if family == 'inet6': - data_lease['last_communication'] = lease.last_communication.timestamp() - data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string) - lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} - data_lease['type'] = lease_types_long[lease.type] - - data_lease['remaining'] = '-' - - if lease.end: - data_lease['remaining'] = lease.end - datetime.utcnow() - - if data_lease['remaining'].days >= 0: - # substraction gives us a timedelta object which can't be formatted with strftime - # so we use str(), split gets rid of the microseconds - data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] - - # Do not add old leases - if data_lease['remaining'] != '' and data_lease['state'] != 'free': - if not state or data_lease['state'] in state or state == 'all': - if not origin or data_lease['origin'] in origin: - if not aux or (aux and data_lease['pool'] == pool_name): - data.append(data_lease) - - # deduplicate - checked = [] - for entry in data: - addr = entry.get('ip') - if addr not in checked: - checked.append(addr) - else: - idx = _find_list_of_dict_index(data, key='ip', value=addr) - data.pop(idx) + + inet_suffix = '6' if family == 'inet6' else '4' + active_config = kea_get_active_config(inet_suffix) + + for lease in leases: + data_lease = {} + data_lease['ip'] = lease['address'] + lease_state_long = {'0': 'active', '1': 'rejected', '2': 'expired'} + data_lease['state'] = lease_state_long[lease['state']] + data_lease['pool'] = kea_get_pool_from_subnet_id(active_config, inet_suffix, lease['subnet_id']) if active_config else '-' + data_lease['end'] = lease['expire_timestamp'].timestamp() if lease['expire_timestamp'] else None + data_lease['origin'] = 'local' # TODO: Determine remote in HA + + if family == 'inet': + data_lease['mac'] = lease['hwaddr'] + data_lease['start'] = lease['start_timestamp'].timestamp() + data_lease['hostname'] = lease['hostname'] + + if family == 'inet6': + data_lease['last_communication'] = lease['start_timestamp'].timestamp() + data_lease['iaid_duid'] = _format_hex_string(lease['duid']) + lease_types_long = {'0': 'non-temporary', '1': 'temporary', '2': 'prefix delegation'} + data_lease['type'] = lease_types_long[lease['lease_type']] + + data_lease['remaining'] = '-' + + if lease['expire']: + data_lease['remaining'] = lease['expire_timestamp'] - datetime.utcnow() + + if data_lease['remaining'].days >= 0: + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] + + # Do not add old leases + if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free': + if not state or state == 'all' or data_lease['state'] in state: + data.append(data_lease) + + # deduplicate + checked = [] + for entry in data: + addr = entry.get('ip') + if addr not in checked: + checked.append(addr) + else: + idx = _find_list_of_dict_index(data, key='ip', value=addr) + data.pop(idx) if sorted: if sorted == 'ip': @@ -282,10 +279,9 @@ def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str], sorted: typing.Optional[str], state: typing.Optional[ArgState], origin: typing.Optional[ArgOrigin] ): # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - v = '6' if family == 'inet6' else '' - service_name = 'DHCPv6' if family == 'inet6' else 'DHCP' - if not is_systemd_service_running(f'isc-dhcp-server{v}.service'): - Warning(f'{service_name} server is configured but not started. Data may be stale.') + v = '6' if family == 'inet6' else '4' + if not is_systemd_service_running(f'kea-dhcp{v}-server.service'): + Warning('DHCP server is configured but not started. Data may be stale.') v = 'v6' if family == 'inet6' else '' if pool and pool not in _get_dhcp_pools(family=family): diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index b3e6e518c..6a8797aec 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -22,6 +22,7 @@ from pathlib import Path from shutil import copy, chown, rmtree, copytree from glob import glob from sys import exit +from os import environ from time import sleep from typing import Union from urllib.parse import urlparse @@ -83,6 +84,8 @@ DIR_KERNEL_SRC: str = '/boot/' FILE_ROOTFS_SRC: str = '/usr/lib/live/mount/medium/live/filesystem.squashfs' ISO_DOWNLOAD_PATH: str = '/tmp/vyos_installation.iso' +external_download_script = '/usr/libexec/vyos/simple-download.py' + # default boot variables DEFAULT_BOOT_VARS: dict[str, str] = { 'timeout': '5', @@ -179,6 +182,7 @@ def create_partitions(target_disk: str, target_size: int, rootfs_size: int = available_size print(MSG_INFO_INSTALL_PARTITONING) + raid.clear() disk.disk_cleanup(target_disk) disk_details: disk.DiskDetails = disk.parttable_create(target_disk, rootfs_size) @@ -459,8 +463,23 @@ def validate_signature(file_path: str, sign_type: str) -> None: else: print('Signature is valid') - -def image_fetch(image_path: str, no_prompt: bool = False) -> Path: +def download_file(local_file: str, remote_path: str, vrf: str, + username: str, password: str, + progressbar: bool = False, check_space: bool = False): + environ['REMOTE_USERNAME'] = username + environ['REMOTE_PASSWORD'] = password + if vrf is None: + download(local_file, remote_path, progressbar=progressbar, + check_space=check_space, raise_error=True) + else: + vrf_cmd = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password} \ + ip vrf exec {vrf} {external_download_script} \ + --local-file {local_file} --remote-path {remote_path}' + cmd(vrf_cmd) + +def image_fetch(image_path: str, vrf: str = None, + username: str = '', password: str = '', + no_prompt: bool = False) -> Path: """Fetch an ISO image Args: @@ -473,14 +492,17 @@ def image_fetch(image_path: str, no_prompt: bool = False) -> Path: # check a type of path if urlparse(image_path).scheme: # download an image - download(ISO_DOWNLOAD_PATH, image_path, True, True, - raise_error=True) + download_file(ISO_DOWNLOAD_PATH, image_path, vrf, + username, password, + progressbar=True, check_space=True) + # download a signature sign_file = (False, '') for sign_type in ['minisig', 'asc']: try: - download(f'{ISO_DOWNLOAD_PATH}.{sign_type}', - f'{image_path}.{sign_type}', raise_error=True) + download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}', + f'{image_path}.{sign_type}', vrf, + username, password) sign_file = (True, sign_type) break except Exception: @@ -501,8 +523,8 @@ def image_fetch(image_path: str, no_prompt: bool = False) -> Path: return local_path else: raise FileNotFoundError - except Exception: - print(f'The image cannot be fetched from: {image_path}') + except Exception as e: + print(f'The image cannot be fetched from: {image_path} {e}') exit(1) @@ -611,7 +633,8 @@ def install_image() -> None: print(MSG_WARN_IMAGE_NAME_WRONG) # ask for password - user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos') + user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos', + no_echo=True) # ask for default console console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, @@ -730,7 +753,8 @@ def install_image() -> None: @compat.grub_cfg_update -def add_image(image_path: str, no_prompt: bool = False) -> None: +def add_image(image_path: str, vrf: str = None, username: str = '', + password: str = '', no_prompt: bool = False) -> None: """Add a new image Args: @@ -740,7 +764,7 @@ def add_image(image_path: str, no_prompt: bool = False) -> None: exit(MSG_ERR_LIVE) # fetch an image - iso_path: Path = image_fetch(image_path, no_prompt) + iso_path: Path = image_fetch(image_path, vrf, username, password, no_prompt) try: # mount an ISO Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True) @@ -840,10 +864,15 @@ def parse_arguments() -> Namespace: choices=['install', 'add'], required=True, help='action to perform with an image') + parser.add_argument('--vrf', + help='vrf name for image download') parser.add_argument('--no-prompt', action='store_true', help='perform action non-interactively') - parser.add_argument( - '--image-path', + parser.add_argument('--username', default='', + help='username for image download') + parser.add_argument('--password', default='', + help='password for image download') + parser.add_argument('--image-path', help='a path (HTTP or local file) to an image that needs to be installed' ) # parser.add_argument('--image_new_name', help='a new name for image') @@ -861,7 +890,8 @@ if __name__ == '__main__': if args.action == 'install': install_image() if args.action == 'add': - add_image(args.image_path, args.no_prompt) + add_image(args.image_path, args.vrf, + args.username, args.password, args.no_prompt) exit() diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 71a40c0e1..2bc7e24fe 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -28,9 +28,6 @@ from vyos.configquery import ConfigTreeQuery from vyos.utils.process import cmd from vyos.utils.dict import dict_search -base = 'nat' -unconf_message = 'NAT is not configured' - ArgDirection = typing.Literal['source', 'destination'] ArgFamily = typing.Literal['inet', 'inet6'] @@ -293,8 +290,9 @@ def _verify(func): @wraps(func) def _wrapper(*args, **kwargs): config = ConfigTreeQuery() + base = 'nat66' if 'inet6' in sys.argv[1:] else 'nat' if not config.exists(base): - raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + raise vyos.opmode.UnconfiguredSubsystem(f'{base.upper()} is not configured') return func(*args, **kwargs) return _wrapper diff --git a/src/pam-configs/mfa-google-authenticator b/src/pam-configs/mfa-google-authenticator new file mode 100644 index 000000000..9e49e5ef9 --- /dev/null +++ b/src/pam-configs/mfa-google-authenticator @@ -0,0 +1,8 @@ +Name: Google Authenticator PAM module (2FA/MFA) +Default: no +Priority: 384 + +Auth-Type: Primary +Auth: + [default=ignore success=ok auth_err=die] pam_google_authenticator.so nullok forward_pass + diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh index 49e53d7e1..7b25bf338 100755 --- a/src/system/on-dhcp-event.sh +++ b/src/system/on-dhcp-event.sh @@ -15,20 +15,20 @@ if [ $# -lt 5 ]; then fi action=$1 -client_name=$2 -client_ip=$3 -client_mac=$4 -domain=$5 +client_name=$LEASE4_HOSTNAME +client_ip=$LEASE4_ADDRESS +client_mac=$LEASE4_HWADDR +domain=$(echo "$client_name" | cut -d"." -f2-) hostsd_client="/usr/bin/vyos-hostsd-client" case "$action" in - commit) # add mapping for new lease + leases4_renew|lease4_recover) # add mapping for new lease if [ -z "$client_name" ]; then logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" client_name=$(echo "client-"$client_mac | tr : -) fi - if [ "$domain" == "..YYZ!" ]; then + if [ -z "$domain" ]; then client_fqdn_name=$client_name client_search_expr=$client_name else @@ -39,7 +39,7 @@ case "$action" in exit 0 ;; - release) # delete mapping for released address + lease4_release|lease4_expire) # delete mapping for released address) $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply exit 0 ;; diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service deleted file mode 100644 index 1345c5fc5..000000000 --- a/src/systemd/isc-dhcp-server6.service +++ /dev/null @@ -1,24 +0,0 @@ -[Unit] -Description=ISC DHCP IPv6 server -Documentation=man:dhcpd(8) -RequiresMountsFor=/run -ConditionPathExists=/run/dhcp-server/dhcpdv6.conf -After=vyos-router.service - -[Service] -Type=forking -WorkingDirectory=/run/dhcp-server -RuntimeDirectory=dhcp-server -RuntimeDirectoryPreserve=yes -Environment=PID_FILE=/run/dhcp-server/dhcpdv6.pid CONFIG_FILE=/run/dhcp-server/dhcpdv6.conf LEASE_FILE=/config/dhcpdv6.leases -PIDFile=/run/dhcp-server/dhcpdv6.pid -ExecStartPre=/bin/sh -ec '\ -touch ${LEASE_FILE}; \ -chown nobody:nogroup ${LEASE_FILE}* ; \ -chmod 664 ${LEASE_FILE}* ; \ -/usr/sbin/dhcpd -6 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' -ExecStart=/usr/sbin/dhcpd -6 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} -Restart=always - -[Install] -WantedBy=multi-user.target diff --git a/src/validators/bgp-large-community-list b/src/validators/bgp-large-community-list index 80112dfdc..9ba5b27eb 100755 --- a/src/validators/bgp-large-community-list +++ b/src/validators/bgp-large-community-list @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 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 @@ -17,9 +17,8 @@ import re import sys -from vyos.template import is_ipv4 - pattern = '(.*):(.*):(.*)' +allowedChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '*', '?', '^', '$', '(', ')', '[', ']', '{', '}', '|', '\\', ':', '-' } if __name__ == '__main__': if len(sys.argv) != 2: @@ -29,8 +28,7 @@ if __name__ == '__main__': if not len(value) == 3: sys.exit(1) - if not (re.match(pattern, sys.argv[1]) and - (is_ipv4(value[0]) or value[0].isdigit()) and (value[1].isdigit() or value[1] == '*')): + if not (re.match(pattern, sys.argv[1]) and set(sys.argv[1]).issubset(allowedChars)): sys.exit(1) sys.exit(0) |