diff options
27 files changed, 707 insertions, 277 deletions
diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 6c0653a55..5538ea56c 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -7,7 +7,7 @@ use{{ ipv }}={{ address if address == 'web' else 'if' }}{{ ipv }}, \ web{{ ipv }}={{ web_options.url }}, \ {% endif %} {% if web_options.skip is vyos_defined %} -web-skip{{ ipv }}='{{ web_options.skip }}', \ +web{{ ipv }}-skip='{{ web_options.skip }}', \ {% endif %} {% else %} if{{ ipv }}={{ address }}, \ @@ -45,9 +45,12 @@ use=no else ['']) %} {% set password = config.key if config.protocol == 'nsupdate' else config.password %} +{% set address = 'web' if config.address.web is vyos_defined + else config.address.interface %} +{% set web_options = config.address.web | default({}) %} # Web service dynamic DNS configuration for {{ service }}: [{{ config.protocol }}, {{ host }}] -{{ render_config(host, config.address, config.web_options, ip_suffixes, +{{ render_config(host, address, web_options, ip_suffixes, protocol=config.protocol, server=config.server, zone=config.zone, login=config.username, password=password, ttl=config.ttl, min_interval=config.wait_time, max_interval=config.expiry_time) }} diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2 index a20c399ae..8a75ab2d6 100644 --- a/data/templates/firewall/nftables-defines.j2 +++ b/data/templates/firewall/nftables-defines.j2 @@ -98,5 +98,26 @@ } {% endfor %} {% endif %} + +{% if group.dynamic_group is vyos_defined %} +{% if group.dynamic_group.address_group is vyos_defined and not is_ipv6 and is_l3 %} +{% for group_name, group_conf in group.dynamic_group.address_group.items() %} + set DA_{{ group_name }} { + type {{ ip_type }} + flags dynamic, timeout + } +{% endfor %} +{% endif %} + +{% if group.dynamic_group.ipv6_address_group is vyos_defined and is_ipv6 and is_l3 %} +{% for group_name, group_conf in group.dynamic_group.ipv6_address_group.items() %} + set DA6_{{ group_name }} { + type {{ ip_type }} + flags dynamic, timeout + } +{% endfor %} +{% endif %} +{% endif %} + {% endif %} {% endmacro %} diff --git a/data/templates/firewall/upnpd.conf.j2 b/data/templates/firewall/upnpd.conf.j2 index e964fc696..616e8869f 100644 --- a/data/templates/firewall/upnpd.conf.j2 +++ b/data/templates/firewall/upnpd.conf.j2 @@ -3,13 +3,42 @@ # WAN network interface ext_ifname={{ wan_interface }} {% if wan_ip is vyos_defined %} + +# if the WAN network interface for IPv6 is different than for IPv4, +# set ext_ifname6 +#ext_ifname6=eth2 + # If the WAN interface has several IP addresses, you -# can specify the one to use below +# can specify the one to use below. +# Setting ext_ip is also useful in double NAT setup, you can declare here +# the public IP address. {% for addr in wan_ip %} ext_ip={{ addr }} {% endfor %} {% endif %} +{% if stun is vyos_defined %} +# WAN interface must have public IP address. Otherwise it is behind NAT +# and port forwarding is impossible. In some cases WAN interface can be +# behind unrestricted full-cone NAT 1:1 when all incoming traffic is NAT-ed and +# routed to WAN interfaces without any filtering. In this cases miniupnpd +# needs to know public IP address and it can be learnt by asking external +# server via STUN protocol. Following option enable retrieving external +# public IP address from STUN server and detection of NAT type. You need +# to specify also external STUN server in stun_host option below. +# This option is disabled by default. +ext_perform_stun=yes +# Specify STUN server, either hostname or IP address +# Some public STUN servers: +# stun.stunprotocol.org +# stun.sipgate.net +# stun.xten.com +# stun.l.google.com (on non standard port 19302) +ext_stun_host={{ stun.host }} +# Specify STUN UDP port, by default it is standard port 3478. +ext_stun_port={{ stun.port }} +{% endif %} + # LAN network interfaces IPs / networks {% if listen is vyos_defined %} # There can be multiple listening IPs for SSDP traffic, in that case @@ -20,6 +49,9 @@ ext_ip={{ addr }} # When MULTIPLE_EXTERNAL_IP is enabled, the external IP # address associated with the subnet follows. For example: # listening_ip=192.168.0.1/24 88.22.44.13 +# When MULTIPLE_EXTERNAL_IP is disabled, you can list associated network +# interfaces (for bridges) +# listening_ip=bridge0 em0 wlan0 {% for addr in listen %} {% if addr | is_ipv4 %} listening_ip={{ addr }} @@ -65,6 +97,18 @@ min_lifetime={{ pcp_lifetime.min }} {% endif %} {% endif %} +# table names for netfilter nft. Default is "filter" for both +#upnp_table_name= +#upnp_nat_table_name= +# chain names for netfilter and netfilter nft +# netfilter : default are MINIUPNPD, MINIUPNPD, MINIUPNPD-POSTROUTING +# netfilter nft : default are miniupnpd, prerouting_miniupnpd, postrouting_miniupnpd +#upnp_forward_chain=forwardUPnP +#upnp_nat_chain=UPnP +#upnp_nat_postrouting_chain=UPnP-Postrouting + +# Lease file location +lease_file=/config/upnp.leases # To enable the next few runtime options, see compile time # ENABLE_MANUFACTURER_INFO_CONFIGURATION (config.h) @@ -89,6 +133,11 @@ model_description=Vyos open source enterprise router/firewall operating system # Model URL, default is URL of OS vendor model_url=https://vyos.io/ +# Bitrates reported by daemon in bits per second +# by default miniupnpd tries to get WAN interface speed +#bitrate_up=1000000 +#bitrate_down=10000000 + {% if secure_mode is vyos_defined %} # Secure Mode, UPnP clients can only add mappings to their own IP secure_mode=yes @@ -108,6 +157,10 @@ secure_mode=no # Report system uptime instead of daemon uptime system_uptime=yes +# Notify interval in seconds. default is 30 seconds. +#notify_interval=240 +notify_interval=60 + # Unused rules cleaning. # never remove any rule before this threshold for the number # of redirections is exceeded. default to 20 @@ -116,25 +169,46 @@ clean_ruleset_threshold=10 # a 600 seconds (10 minutes) interval makes sense clean_ruleset_interval=600 +############################################################################ +## The next 5 config parameters (packet_log, anchor, queue, tag, quickrules) +## are specific to BSD's pf(4) packet filter and hence cannot be enabled in +## VyOS. +# Log packets in pf (default is no) +#packet_log=no + # Anchor name in pf (default is miniupnpd) -# Something wrong with this option "anchor", comment it out -# vyos@r14# miniupnpd -vv -f /run/upnp/miniupnp.conf -# invalid option in file /run/upnp/miniupnp.conf line 74 : anchor=VyOS -#anchor=VyOS +#anchor=miniupnpd -uuid={{ uuid }} +# ALTQ queue in pf +# Filter rules must be used for this to be used. +# compile with PF_ENABLE_FILTER_RULES (see config.h file) +#queue=queue_name1 -# Lease file location -lease_file=/config/upnp.leases +# Tag name in pf +#tag=tag_name1 + +# Make filter rules in pf quick or not. default is yes +# active when compiled with PF_ENABLE_FILTER_RULES (see config.h file) +#quickrules=no +## +## End of pf(4)-specific configuration not to be set in VyOS. +############################################################################ + +# UUID, generate your own UUID with "make genuuid" +uuid={{ uuid }} # Daemon's serial and model number when reporting to clients # (in XML description) #serial=12345678 #model_number=1 +# If compiled with IGD_V2 defined, force reporting IGDv1 in rootDesc (default +# is no) +#force_igd_desc_v1=no + {% if rule is vyos_defined %} -# UPnP permission rules -# (allow|deny) (external port range) IP/mask (internal port range) +# UPnP permission rules (also enforced for NAT-PMP and PCP) +# (allow|deny) (external port range) IP/mask (internal port range) (optional regex filter) # A port range is <min port>-<max port> or <port> if there is only # one port in the range. # IP/mask format must be nnn.nnn.nnn.nnn/nn @@ -151,25 +225,3 @@ lease_file=/config/upnp.leases {% endif %} {% endfor %} {% endif %} - -{% if stun is vyos_defined %} -# WAN interface must have public IP address. Otherwise it is behind NAT -# and port forwarding is impossible. In some cases WAN interface can be -# behind unrestricted NAT 1:1 when all incoming traffic is NAT-ed and -# routed to WAN interfaces without any filtering. In this cases miniupnpd -# needs to know public IP address and it can be learnt by asking external -# server via STUN protocol. Following option enable retrieving external -# public IP address from STUN server and detection of NAT type. You need -# to specify also external STUN server in stun_host option below. -# This option is disabled by default. -ext_perform_stun=yes -# Specify STUN server, either hostname or IP address -# Some public STUN servers: -# stun.stunprotocol.org -# stun.sipgate.net -# stun.xten.com -# stun.l.google.com (on non standard port 19302) -ext_stun_host={{ stun.host }} -# Specify STUN UDP port, by default it is standard port 3478. -ext_stun_port={{ stun.port }} -{% endif %} diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index a4023058f..662ba24ab 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -115,6 +115,35 @@ #include <include/generic-description.xml.i> </children> </tagNode> + <node name="dynamic-group"> + <properties> + <help>Firewall dynamic group</help> + </properties> + <children> + <tagNode name="address-group"> + <properties> + <help>Firewall dynamic address group</help> + <constraint> + <regex>[a-zA-Z0-9][\w\-\.]*</regex> + </constraint> + </properties> + <children> + #include <include/generic-description.xml.i> + </children> + </tagNode> + <tagNode name="ipv6-address-group"> + <properties> + <help>Firewall dynamic IPv6 address group</help> + <constraint> + <regex>[a-zA-Z0-9][\w\-\.]*</regex> + </constraint> + </properties> + <children> + #include <include/generic-description.xml.i> + </children> + </tagNode> + </children> + </node> <tagNode name="interface-group"> <properties> <help>Firewall interface-group</help> diff --git a/interface-definitions/include/firewall/add-dynamic-address-groups.xml.i b/interface-definitions/include/firewall/add-dynamic-address-groups.xml.i new file mode 100644 index 000000000..769761cb6 --- /dev/null +++ b/interface-definitions/include/firewall/add-dynamic-address-groups.xml.i @@ -0,0 +1,34 @@ +<!-- include start from firewall/add-dynamic-address-groups.xml.i --> +<leafNode name="address-group"> + <properties> + <help>Dynamic address-group</help> + <completionHelp> + <path>firewall group dynamic-group address-group</path> + </completionHelp> + </properties> +</leafNode> +<leafNode name="timeout"> + <properties> + <help>Set timeout</help> + <valueHelp> + <format><number>s</format> + <description>Timeout value in seconds</description> + </valueHelp> + <valueHelp> + <format><number>m</format> + <description>Timeout value in minutes</description> + </valueHelp> + <valueHelp> + <format><number>h</format> + <description>Timeout value in hours</description> + </valueHelp> + <valueHelp> + <format><number>d</format> + <description>Timeout value in days</description> + </valueHelp> + <constraint> + <regex>\d+(s|m|h|d)</regex> + </constraint> + </properties> +</leafNode> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/add-dynamic-ipv6-address-groups.xml.i b/interface-definitions/include/firewall/add-dynamic-ipv6-address-groups.xml.i new file mode 100644 index 000000000..7bd91c58a --- /dev/null +++ b/interface-definitions/include/firewall/add-dynamic-ipv6-address-groups.xml.i @@ -0,0 +1,34 @@ +<!-- include start from firewall/add-dynamic-ipv6-address-groups.xml.i --> +<leafNode name="address-group"> + <properties> + <help>Dynamic ipv6-address-group</help> + <completionHelp> + <path>firewall group dynamic-group ipv6-address-group</path> + </completionHelp> + </properties> +</leafNode> +<leafNode name="timeout"> + <properties> + <help>Set timeout</help> + <valueHelp> + <format><number>s</format> + <description>Timeout value in seconds</description> + </valueHelp> + <valueHelp> + <format><number>m</format> + <description>Timeout value in minutes</description> + </valueHelp> + <valueHelp> + <format><number>h</format> + <description>Timeout value in hours</description> + </valueHelp> + <valueHelp> + <format><number>d</format> + <description>Timeout value in days</description> + </valueHelp> + <constraint> + <regex>\d+(s|m|h|d)</regex> + </constraint> + </properties> +</leafNode> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/common-rule-ipv4.xml.i b/interface-definitions/include/firewall/common-rule-ipv4.xml.i index 4ed179ae7..158c7a662 100644 --- a/interface-definitions/include/firewall/common-rule-ipv4.xml.i +++ b/interface-definitions/include/firewall/common-rule-ipv4.xml.i @@ -1,6 +1,29 @@ <!-- include start from firewall/common-rule-ipv4.xml.i --> #include <include/firewall/common-rule-inet.xml.i> #include <include/firewall/ttl.xml.i> +<node name="add-address-to-group"> + <properties> + <help>Add ip address to dynamic address-group</help> + </properties> + <children> + <node name="source-address"> + <properties> + <help>Add source ip addresses to dynamic address-group</help> + </properties> + <children> + #include <include/firewall/add-dynamic-address-groups.xml.i> + </children> + </node> + <node name="destination-address"> + <properties> + <help>Add destination ip addresses to dynamic address-group</help> + </properties> + <children> + #include <include/firewall/add-dynamic-address-groups.xml.i> + </children> + </node> + </children> +</node> <node name="destination"> <properties> <help>Destination parameters</help> @@ -13,6 +36,7 @@ #include <include/firewall/mac-address.xml.i> #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group.xml.i> + #include <include/firewall/source-destination-dynamic-group.xml.i> </children> </node> <node name="icmp"> @@ -67,6 +91,7 @@ #include <include/firewall/mac-address.xml.i> #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group.xml.i> + #include <include/firewall/source-destination-dynamic-group.xml.i> </children> </node> <!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/common-rule-ipv6.xml.i b/interface-definitions/include/firewall/common-rule-ipv6.xml.i index 6219557db..78eeb361e 100644 --- a/interface-definitions/include/firewall/common-rule-ipv6.xml.i +++ b/interface-definitions/include/firewall/common-rule-ipv6.xml.i @@ -1,6 +1,29 @@ <!-- include start from firewall/common-rule-ipv6.xml.i --> #include <include/firewall/common-rule-inet.xml.i> #include <include/firewall/hop-limit.xml.i> +<node name="add-address-to-group"> + <properties> + <help>Add ipv6 address to dynamic ipv6-address-group</help> + </properties> + <children> + <node name="source-address"> + <properties> + <help>Add source ipv6 addresses to dynamic ipv6-address-group</help> + </properties> + <children> + #include <include/firewall/add-dynamic-ipv6-address-groups.xml.i> + </children> + </node> + <node name="destination-address"> + <properties> + <help>Add destination ipv6 addresses to dynamic ipv6-address-group</help> + </properties> + <children> + #include <include/firewall/add-dynamic-ipv6-address-groups.xml.i> + </children> + </node> + </children> +</node> <node name="destination"> <properties> <help>Destination parameters</help> @@ -13,6 +36,7 @@ #include <include/firewall/mac-address.xml.i> #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> + #include <include/firewall/source-destination-dynamic-group-ipv6.xml.i> </children> </node> <node name="icmpv6"> @@ -67,6 +91,7 @@ #include <include/firewall/mac-address.xml.i> #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> + #include <include/firewall/source-destination-dynamic-group-ipv6.xml.i> </children> </node> <!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/firewall/source-destination-dynamic-group-ipv6.xml.i b/interface-definitions/include/firewall/source-destination-dynamic-group-ipv6.xml.i new file mode 100644 index 000000000..845f8fe7c --- /dev/null +++ b/interface-definitions/include/firewall/source-destination-dynamic-group-ipv6.xml.i @@ -0,0 +1,17 @@ +<!-- include start from firewall/source-destination-dynamic-group-ipv6.xml.i --> +<node name="group"> + <properties> + <help>Group</help> + </properties> + <children> + <leafNode name="dynamic-address-group"> + <properties> + <help>Group of dynamic ipv6 addresses</help> + <completionHelp> + <path>firewall group dynamic-group ipv6-address-group</path> + </completionHelp> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/firewall/source-destination-dynamic-group.xml.i b/interface-definitions/include/firewall/source-destination-dynamic-group.xml.i new file mode 100644 index 000000000..29ab98c68 --- /dev/null +++ b/interface-definitions/include/firewall/source-destination-dynamic-group.xml.i @@ -0,0 +1,17 @@ +<!-- include start from firewall/source-destination-dynamic-group.xml.i --> +<node name="group"> + <properties> + <help>Group</help> + </properties> + <children> + <leafNode name="dynamic-address-group"> + <properties> + <help>Group of dynamic addresses</help> + <completionHelp> + <path>firewall group dynamic-group address-group</path> + </completionHelp> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/version/dns-dynamic-version.xml.i b/interface-definitions/include/version/dns-dynamic-version.xml.i index 773a6ab51..346385ccb 100644 --- a/interface-definitions/include/version/dns-dynamic-version.xml.i +++ b/interface-definitions/include/version/dns-dynamic-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dns-dynamic-version.xml.i --> -<syntaxVersion component='dns-dynamic' version='3'></syntaxVersion> +<syntaxVersion component='dns-dynamic' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/service_dns_dynamic.xml.in b/interface-definitions/service_dns_dynamic.xml.in index d1b0e90bb..75e5520b7 100644 --- a/interface-definitions/service_dns_dynamic.xml.in +++ b/interface-definitions/service_dns_dynamic.xml.in @@ -38,42 +38,29 @@ </constraint> </properties> </leafNode> - <leafNode name="address"> + <node name="address"> <properties> <help>Obtain IP address to send Dynamic DNS update for</help> - <valueHelp> - <format>txt</format> - <description>Use interface to obtain the IP address</description> - </valueHelp> - <valueHelp> - <format>web</format> - <description>Use HTTP(S) web request to obtain the IP address</description> - </valueHelp> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - <list>web</list> - </completionHelp> - <constraint> - #include <include/constraint/interface-name.xml.i> - <regex>web</regex> - </constraint> - </properties> - </leafNode> - <node name="web-options"> - <properties> - <help>Options when using HTTP(S) web request to obtain the IP address</help> </properties> <children> - #include <include/url-http-https.xml.i> - <leafNode name="skip"> + #include <include/generic-interface.xml.i> + <node name="web"> <properties> - <help>Pattern to skip from the HTTP(S) respose</help> - <valueHelp> - <format>txt</format> - <description>Pattern to skip from the HTTP(S) respose to extract the external IP address</description> - </valueHelp> + <help>HTTP(S) web request to use</help> </properties> - </leafNode> + <children> + #include <include/url-http-https.xml.i> + <leafNode name="skip"> + <properties> + <help>Pattern to skip from the HTTP(S) respose</help> + <valueHelp> + <format>txt</format> + <description>Pattern to skip from the HTTP(S) respose to extract the external IP address</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> </children> </node> <leafNode name="ip-version"> diff --git a/interface-definitions/service_upnp.xml.in b/interface-definitions/service_upnp.xml.in index 20e01bfbd..064386ee5 100644 --- a/interface-definitions/service_upnp.xml.in +++ b/interface-definitions/service_upnp.xml.in @@ -205,6 +205,7 @@ <constraint> <validator name="ipv4-address"/> <validator name="ipv4-host"/> + <validator name="ipv4-prefix"/> </constraint> </properties> </leafNode> diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index f581d39fa..96c582a83 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -154,6 +154,9 @@ </children> </node> <node name="update"> + <properties> + <help>Update data for a service</help> + </properties> <children> <node name="container"> <properties> diff --git a/op-mode-definitions/dns-dynamic.xml.in b/op-mode-definitions/dns-dynamic.xml.in index 79478f392..45d58e2e8 100644 --- a/op-mode-definitions/dns-dynamic.xml.in +++ b/op-mode-definitions/dns-dynamic.xml.in @@ -4,7 +4,7 @@ <children> <node name="dns"> <properties> - <help>Clear Domain Name System</help> + <help>Clear Domain Name System (DNS) related service state</help> </properties> <children> <node name="dynamic"> @@ -30,7 +30,7 @@ <children> <node name="dns"> <properties> - <help>Monitor last lines of Domain Name System related services</help> + <help>Monitor last lines of Domain Name System (DNS) related services</help> </properties> <children> <node name="dynamic"> @@ -51,7 +51,7 @@ <children> <node name="dns"> <properties> - <help>Show log for Domain Name System related services</help> + <help>Show log for Domain Name System (DNS) related services</help> </properties> <children> <node name="dynamic"> @@ -66,7 +66,7 @@ </node> <node name="dns"> <properties> - <help>Show Domain Name System related information</help> + <help>Show Domain Name System (DNS) related information</help> </properties> <children> <node name="dynamic"> @@ -78,7 +78,7 @@ <properties> <help>Show Dynamic DNS status</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --status</command> + <command>sudo ${vyos_op_scripts_dir}/dns.py show_dynamic_status</command> </leafNode> </children> </node> @@ -90,34 +90,31 @@ <children> <node name="dns"> <properties> - <help>Restart specific Domain Name System related service</help> + <help>Restart specific Domain Name System (DNS) related service</help> </properties> <children> <node name="dynamic"> <properties> <help>Restart Dynamic DNS service</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update</command> + <command>if cli-shell-api existsActive service dns dynamic; then sudo systemctl restart ddclient.service; else echo "Dynamic DNS not configured"; fi</command> </node> </children> </node> </children> </node> - <node name="update"> - <properties> - <help>Update data for a service</help> - </properties> + <node name="reset"> <children> <node name="dns"> <properties> - <help>Update Domain Name System related information</help> + <help>Reset Domain Name System (DNS) related service state</help> </properties> <children> <node name="dynamic"> <properties> - <help>Update Dynamic DNS information</help> + <help>Reset Dynamic DNS information</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update</command> + <command>sudo ${vyos_op_scripts_dir}/dns.py reset_dynamic</command> </node> </children> </node> diff --git a/op-mode-definitions/dns-forwarding.xml.in b/op-mode-definitions/dns-forwarding.xml.in index ebedae6eb..29bfc61cf 100644 --- a/op-mode-definitions/dns-forwarding.xml.in +++ b/op-mode-definitions/dns-forwarding.xml.in @@ -11,7 +11,7 @@ <children> <node name="forwarding"> <properties> - <help>Monitor last lines of DNS Forwarding</help> + <help>Monitor last lines of DNS Forwarding service</help> </properties> <command>journalctl --no-hostname --follow --boot --unit pdns-recursor.service</command> </node> diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 28ebf282c..eee11bd2d 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -226,6 +226,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): operator = '!=' if exclude else '==' operator = f'& {address_mask} {operator}' output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}') + elif 'dynamic_address_group' in group: + group_name = group['dynamic_address_group'] + operator = '' + exclude = group_name[0] == "!" + if exclude: + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_name} {prefix}addr {operator} @DA{def_suffix}_{group_name}') # Generate firewall group domain-group elif 'domain_group' in group: group_name = group['domain_group'] @@ -419,6 +427,18 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): output.append('counter') + if 'add_address_to_group' in rule_conf: + for side in ['destination_address', 'source_address']: + if side in rule_conf['add_address_to_group']: + prefix = side[0] + side_conf = rule_conf['add_address_to_group'][side] + dyn_group = side_conf['address_group'] + if 'timeout' in side_conf: + timeout_value = side_conf['timeout'] + output.append(f'set update ip{def_suffix} {prefix}addr timeout {timeout_value} @DA{def_suffix}_{dyn_group}') + else: + output.append(f'set update ip{def_suffix} saddr @DA{def_suffix}_{dyn_group}') + if 'set' in rule_conf: output.append(parse_policy_set(rule_conf['set'], def_suffix)) diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 230a85541..e1af1a682 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2022-2024 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 @@ -81,7 +81,7 @@ class InternalError(Error): def _is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart|add|delete|generate|set)", name): + if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set)", name): return True else: return False @@ -275,4 +275,3 @@ def run(module): # Other functions should not return anything, # although they may print their own warnings or status messages func(**args) - diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 72fbdb37d..a7dd11145 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -403,6 +403,46 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') + def test_ipv4_dynamic_groups(self): + group01 = 'knock01' + group02 = 'allowed' + + self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group01]) + self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group02]) + + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'port', '5151']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'address-group', group01]) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'timeout', '30s']) + + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'action', 'drop']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'destination', 'port', '7272']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'source', 'group', 'dynamic-address-group', group01]) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'address-group', group02]) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'timeout', '5m']) + + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'action', 'accept']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'destination', 'port', '22']) + self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02]) + + self.cli_commit() + + nftables_search = [ + [f'DA_{group01}'], + [f'DA_{group02}'], + ['type ipv4_addr'], + ['flags dynamic,timeout'], + ['chain VYOS_INPUT_filter {'], + ['type filter hook input priority filter', 'policy accept'], + ['tcp dport 5151', f'update @DA_{group01}', '{ ip saddr timeout 30s }', 'drop'], + ['tcp dport 7272', f'ip saddr @DA_{group01}', f'update @DA_{group02}', '{ ip saddr timeout 5m }', 'drop'], + ['tcp dport 22', f'ip saddr @DA_{group02}', 'accept'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv6_basic_rules(self): name = 'v6-smoketest' @@ -540,6 +580,47 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_filter') + def test_ipv6_dynamic_groups(self): + group01 = 'knock01' + group02 = 'allowed' + + self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group01]) + self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group02]) + + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'port', '5151']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'address-group', group01]) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'timeout', '30s']) + + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'destination', 'port', '7272']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'source', 'group', 'dynamic-address-group', group01]) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'address-group', group02]) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'timeout', '5m']) + + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'action', 'accept']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'protocol', 'tcp']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'destination', 'port', '22']) + self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02]) + + self.cli_commit() + + nftables_search = [ + [f'DA6_{group01}'], + [f'DA6_{group02}'], + ['type ipv6_addr'], + ['flags dynamic,timeout'], + ['chain VYOS_IPV6_INPUT_filter {'], + ['type filter hook input priority filter', 'policy accept'], + ['tcp dport 5151', f'update @DA6_{group01}', '{ ip6 saddr timeout 30s }', 'drop'], + ['tcp dport 7272', f'ip6 saddr @DA6_{group01}', f'update @DA6_{group02}', '{ ip6 saddr timeout 5m }', 'drop'], + ['tcp dport 22', f'ip6 saddr @DA6_{group02}', 'accept'] + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_filter') + def test_ipv4_state_and_status_rules(self): name = 'smoketest-state' interface = 'eth0' diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index ae46b18ba..c39d4467a 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 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 @@ -62,7 +62,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): 'zoneedit': {'protocol': 'zoneedit1', 'username': username}} for svc, details in services.items(): - self.cli_set(name_path + [svc, 'address', interface]) + self.cli_set(name_path + [svc, 'address', 'interface', interface]) self.cli_set(name_path + [svc, 'host-name', hostname]) self.cli_set(name_path + [svc, 'password', password]) for opt, value in details.items(): @@ -118,7 +118,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): expiry_time_bad = '360' self.cli_set(base_path + ['interval', interval]) - self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['address', 'interface', interface]) self.cli_set(svc_path + ['ip-version', ip_version]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['server', server]) @@ -156,7 +156,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): ip_version = 'both' for name, details in services.items(): - self.cli_set(name_path + [name, 'address', interface]) + self.cli_set(name_path + [name, 'address', 'interface', interface]) self.cli_set(name_path + [name, 'host-name', hostname]) self.cli_set(name_path + [name, 'password', password]) for opt, value in details.items(): @@ -201,7 +201,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file: key_file.write(b'S3cretKey') - self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['address', 'interface', interface]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['server', server]) self.cli_set(svc_path + ['zone', zone]) @@ -229,7 +229,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): hostnames = ['@', 'www', hostname, f'@.{hostname}'] for name in hostnames: - self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['address', 'interface', interface]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['server', server]) self.cli_set(svc_path + ['username', username]) @@ -251,38 +251,38 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check if DDNS service can be configured and runs svc_path = name_path + ['cloudflare'] proto = 'cloudflare' - web_url_good = 'https://ifconfig.me/ip' - web_url_bad = 'http:/ifconfig.me/ip' + web_url = 'https://ifconfig.me/ip' + web_skip = 'Current IP Address:' self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['zone', zone]) self.cli_set(svc_path + ['password', password]) self.cli_set(svc_path + ['host-name', hostname]) - self.cli_set(svc_path + ['web-options', 'url', web_url_good]) - # web-options is supported only with web service based address lookup - # exception is raised for interface based address lookup + # not specifying either 'interface' or 'web' will raise an exception with self.assertRaises(ConfigSessionError): - self.cli_set(svc_path + ['address', interface]) self.cli_commit() self.cli_set(svc_path + ['address', 'web']) - # commit changes + # specifying both 'interface' and 'web' will raise an exception as well + with self.assertRaises(ConfigSessionError): + self.cli_set(svc_path + ['address', 'interface', interface]) + self.cli_commit() + self.cli_delete(svc_path + ['address', 'interface']) self.cli_commit() - # web-options must be a valid URL + # web option 'skip' is useless without the option 'url' with self.assertRaises(ConfigSessionError): - self.cli_set(svc_path + ['web-options', 'url', web_url_bad]) + self.cli_set(svc_path + ['address', 'web', 'skip', web_skip]) self.cli_commit() - self.cli_set(svc_path + ['web-options', 'url', web_url_good]) - - # commit changes + self.cli_set(svc_path + ['address', 'web', 'url', web_url]) self.cli_commit() # Check the generating config parameters ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') self.assertIn(f'usev4=webv4', ddclient_conf) - self.assertIn(f'webv4={web_url_good}', ddclient_conf) + self.assertIn(f'webv4={web_url}', ddclient_conf) + self.assertIn(f'webv4-skip=\'{web_skip}\'', ddclient_conf) self.assertIn(f'protocol={proto}', ddclient_conf) self.assertIn(f'zone={zone}', ddclient_conf) self.assertIn(f'password=\'{password}\'', ddclient_conf) @@ -294,7 +294,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): proto = 'namecheap' dyn_interface = 'pppoe587' - self.cli_set(svc_path + ['address', dyn_interface]) + self.cli_set(svc_path + ['address', 'interface', dyn_interface]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['server', server]) self.cli_set(svc_path + ['username', username]) @@ -327,7 +327,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_set(['vrf', 'name', vrf_name, 'table', vrf_table]) self.cli_set(base_path + ['vrf', vrf_name]) - self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['address', 'interface', interface]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['host-name', hostname]) self.cli_set(svc_path + ['zone', zone]) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index bd9b5162c..26822b755 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -69,6 +69,10 @@ def get_config(config=None): nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + # Remove dynamic firewall groups if present: + if 'dynamic_group' in nat['firewall_group']: + del nat['firewall_group']['dynamic_group'] + return nat def verify_rule(config, err_msg, groups_dict): diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py index adad012de..6d7a06714 100755 --- a/src/conf_mode/policy_route.py +++ b/src/conf_mode/policy_route.py @@ -53,6 +53,10 @@ def get_config(config=None): policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + # Remove dynamic firewall groups if present: + if 'dynamic_group' in policy['firewall_group']: + del policy['firewall_group']['dynamic_group'] + return policy def verify_rule(policy, name, rule_conf, ipv6, rule_id): diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index 845aaa1b5..a551a9891 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 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 @@ -87,31 +87,36 @@ def verify(dyndns): if field not in config: raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') - # 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': + if not any(x in config['address'] for x in ['interface', 'web']): + raise ConfigError(f'Either "interface" or "web" {error_msg_req} ' + f'with protocol "{config["protocol"]}"') + if all(x in config['address'] for x in ['interface', 'web']): + raise ConfigError(f'Both "interface" and "web" at the same time {error_msg_uns} ' + f'with protocol "{config["protocol"]}"') + + # If dyndns address is an interface, ensure that the interface exists + # and warn if a non-active dynamic interface is used + if 'interface' in config['address']: tmp = re.compile(dynamic_interface_pattern) # exclude check interface for dynamic interfaces - if tmp.match(config["address"]): - if not interface_exists(config["address"]): - Warning(f'Interface "{config["address"]}" does not exist yet and cannot ' - f'be used for Dynamic DNS service "{service}" until it is up!') + if tmp.match(config['address']['interface']): + if not interface_exists(config['address']['interface']): + Warning(f'Interface "{config["address"]["interface"]}" does not exist yet and ' + f'cannot 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) ' - f'web request to obtain the IP address') - - # Warn if using checkip.dyndns.org, as it does not support HTTPS - # See: https://github.com/ddclient/ddclient/issues/597 - if 'web_options' in config: - if 'url' not in config['web_options']: - raise ConfigError(f'"url" in "web-options" {error_msg_req} ' + verify_interface_exists(config['address']['interface']) + + if 'web' in config['address']: + # If 'skip' is specified, 'url' is required as well + if 'skip' in config['address']['web'] and 'url' not in config['address']['web']: + raise ConfigError(f'"url" along with "skip" {error_msg_req} ' f'with protocol "{config["protocol"]}"') - elif re.search("^(https?://)?checkip\.dyndns\.org", config['web_options']['url']): - Warning(f'"checkip.dyndns.org" does not support HTTPS requests for IP address ' - f'lookup. Please use a different IP address lookup service.') + if 'url' in config['address']['web']: + # Warn if using checkip.dyndns.org, as it does not support HTTPS + # See: https://github.com/ddclient/ddclient/issues/597 + if re.search("^(https?://)?checkip\.dyndns\.org", config['address']['web']['url']): + Warning(f'"checkip.dyndns.org" does not support HTTPS requests for IP address ' + f'lookup. Please use a different IP address lookup service.') # RFC2136 uses 'key' instead of 'password' if config['protocol'] != 'nsupdate' and 'password' not in config: diff --git a/src/migration-scripts/dns-dynamic/3-to-4 b/src/migration-scripts/dns-dynamic/3-to-4 new file mode 100755 index 000000000..b888a3b6b --- /dev/null +++ b/src/migration-scripts/dns-dynamic/3-to-4 @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2024 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/>. + +# T5966: +# - migrate "service dns dynamic name <service> address <interface>" +# to "service dns dynamic name <service> address interface <interface>" +# when <interface> != 'web' +# - migrate "service dns dynamic name <service> web-options ..." +# to "service dns dynamic name <service> address web ..." +# when <interface> == 'web' + +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() + +config = ConfigTree(config_file) + +base_path = ['service', 'dns', 'dynamic', 'name'] + +if not config.exists(base_path): + # Nothing to do + sys.exit(0) + +for service in config.list_nodes(base_path): + + service_path = base_path + [service] + + if config.exists(service_path + ['address']): + address = config.return_value(service_path + ['address']) + # 'address' is not a leaf node anymore, delete it first + config.delete(service_path + ['address']) + + # When address is an interface (not 'web'), move it to 'address interface' + if address != 'web': + config.set(service_path + ['address', 'interface'], address) + + else: # address == 'web' + # Relocate optional 'web-options' directly under 'address web' + if config.exists(service_path + ['web-options']): + # config.copy does not recursively create a path, so initialize it + config.set(service_path + ['address']) + config.copy(service_path + ['web-options'], + service_path + ['address', 'web']) + config.delete(service_path + ['web-options']) + + # ensure that valueless 'address web' still exists even if there are no 'web-options' + if not config.exists(service_path + ['address', 'web']): + config.set(service_path + ['address', 'web']) + +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/op_mode/dns.py b/src/op_mode/dns.py index 309bef3b9..16c462f23 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -15,14 +15,33 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. -import typing +import os import sys +import time +import typing import vyos.opmode from tabulate import tabulate from vyos.configquery import ConfigTreeQuery from vyos.utils.process import cmd, rc_cmd +from vyos.template import is_ipv4, is_ipv6 + +_dynamic_cache_file = r'/run/ddclient/ddclient.cache' + +_dynamic_status_columns = { + 'host': 'Hostname', + 'ipv4': 'IPv4 address', + 'status-ipv4': 'IPv4 status', + 'ipv6': 'IPv6 address', + 'status-ipv6': 'IPv6 status', + 'mtime': 'Last update', +} +_forwarding_statistics_columns = { + 'cache-entries': 'Cache entries', + 'max-cache-entries': 'Max cache entries', + 'cache-size': 'Cache size', +} def _forwarding_data_to_dict(data, sep="\t") -> dict: """ @@ -50,37 +69,106 @@ def _forwarding_data_to_dict(data, sep="\t") -> dict: dictionary[key] = value return dictionary +def _get_dynamic_host_records_raw() -> dict: + + data = [] + + if os.path.isfile(_dynamic_cache_file): # A ddclient status file might not always exist + with open(_dynamic_cache_file, 'r') as f: + for line in f: + if line.startswith('#'): + continue + + props = {} + # ddclient cache rows have properties in 'key=value' format separated by comma + # we pick up the ones we are interested in + for kvraw in line.split(' ')[0].split(','): + k, v = kvraw.split('=') + if k in list(_dynamic_status_columns.keys()) + ['ip', 'status']: # ip and status are legacy keys + props[k] = v + + # Extract IPv4 and IPv6 address and status from legacy keys + # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6 + if 'ip' in props: + if is_ipv4(props['ip']): + props['ipv4'] = props['ip'] + props['status-ipv4'] = props['status'] + elif is_ipv6(props['ip']): + props['ipv6'] = props['ip'] + props['status-ipv6'] = props['status'] + del props['ip'] + + # Convert mtime to human readable format + if 'mtime' in props: + props['mtime'] = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) + + data.append(props) + + return data + +def _get_dynamic_host_records_formatted(data): + data_entries = [] + for entry in data: + data_entries.append([entry.get(key) for key in _dynamic_status_columns.keys()]) + header = _dynamic_status_columns.values() + output = tabulate(data_entries, header, numalign='left') + return output def _get_forwarding_statistics_raw() -> dict: command = cmd('rec_control get-all') data = _forwarding_data_to_dict(command) - data['cache-size'] = "{0:.2f}".format( int( + data['cache-size'] = "{0:.2f} kbytes".format( int( cmd('rec_control get cache-bytes')) / 1024 ) return data - def _get_forwarding_statistics_formatted(data): - cache_entries = data.get('cache-entries') - max_cache_entries = data.get('max-cache-entries') - cache_size = data.get('cache-size') - data_entries = [[cache_entries, max_cache_entries, f'{cache_size} kbytes']] - headers = ["Cache entries", "Max cache entries" , "Cache size"] - output = tabulate(data_entries, headers, numalign="left") + data_entries = [] + data_entries.append([data.get(key) for key in _forwarding_statistics_columns.keys()]) + header = _forwarding_statistics_columns.values() + output = tabulate(data_entries, header, numalign='left') return output -def _verify_forwarding(func): - """Decorator checks if DNS Forwarding config exists""" +def _verify(target): + """Decorator checks if config for DNS related service exists""" from functools import wraps - @wraps(func) - def _wrapper(*args, **kwargs): - config = ConfigTreeQuery() - if not config.exists('service dns forwarding'): - raise vyos.opmode.UnconfiguredSubsystem('DNS Forwarding is not configured') - return func(*args, **kwargs) - return _wrapper + if target not in ['dynamic', 'forwarding']: + raise ValueError('Invalid target') + + def _verify_target(func): + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + if not config.exists(f'service dns {target}'): + _prefix = f'Dynamic DNS' if target == 'dynamic' else 'DNS Forwarding' + raise vyos.opmode.UnconfiguredSubsystem(f'{_prefix} is not configured') + return func(*args, **kwargs) + return _wrapper + return _verify_target + +@_verify('dynamic') +def show_dynamic_status(raw: bool): + host_data = _get_dynamic_host_records_raw() + if raw: + return host_data + else: + return _get_dynamic_host_records_formatted(host_data) -@_verify_forwarding +@_verify('dynamic') +def reset_dynamic(): + """ + Reset Dynamic DNS cache + """ + if os.path.exists(_dynamic_cache_file): + os.remove(_dynamic_cache_file) + rc, output = rc_cmd('systemctl restart ddclient.service') + if rc != 0: + print(output) + return None + print(f'Dynamic DNS state reset!') + +@_verify('forwarding') def show_forwarding_statistics(raw: bool): dns_data = _get_forwarding_statistics_raw() if raw: @@ -88,7 +176,7 @@ def show_forwarding_statistics(raw: bool): else: return _get_forwarding_statistics_formatted(dns_data) -@_verify_forwarding +@_verify('forwarding') def reset_forwarding(all: bool, domain: typing.Optional[str]): """ Reset DNS Forwarding cache diff --git a/src/op_mode/dns_dynamic.py b/src/op_mode/dns_dynamic.py deleted file mode 100755 index 12aa5494a..000000000 --- a/src/op_mode/dns_dynamic.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -# -# 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 -# 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 argparse -import sys -import time -from tabulate import tabulate - -from vyos.config import Config -from vyos.template import is_ipv4, is_ipv6 -from vyos.utils.process import call - -cache_file = r'/run/ddclient/ddclient.cache' - -columns = { - 'host': 'Hostname', - 'ipv4': 'IPv4 address', - 'status-ipv4': 'IPv4 status', - 'ipv6': 'IPv6 address', - 'status-ipv6': 'IPv6 status', - 'mtime': 'Last update', -} - - -def _get_formatted_host_records(host_data): - data_entries = [] - for entry in host_data: - data_entries.append([entry.get(key) for key in columns.keys()]) - - header = columns.values() - output = tabulate(data_entries, header, numalign='left') - return output - - -def show_status(): - # A ddclient status file might not always exist - if not os.path.exists(cache_file): - sys.exit(0) - - data = [] - - with open(cache_file, 'r') as f: - for line in f: - if line.startswith('#'): - continue - - props = {} - # ddclient cache rows have properties in 'key=value' format separated by comma - # we pick up the ones we are interested in - for kvraw in line.split(' ')[0].split(','): - k, v = kvraw.split('=') - if k in list(columns.keys()) + ['ip', 'status']: # ip and status are legacy keys - props[k] = v - - # Extract IPv4 and IPv6 address and status from legacy keys - # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6 - if 'ip' in props: - if is_ipv4(props['ip']): - props['ipv4'] = props['ip'] - props['status-ipv4'] = props['status'] - elif is_ipv6(props['ip']): - props['ipv6'] = props['ip'] - props['status-ipv6'] = props['status'] - del props['ip'] - - # Convert mtime to human readable format - if 'mtime' in props: - props['mtime'] = time.strftime( - "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) - - data.append(props) - - print(_get_formatted_host_records(data)) - - -def update_ddns(): - call('systemctl stop ddclient.service') - if os.path.exists(cache_file): - os.remove(cache_file) - call('systemctl start ddclient.service') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument("--status", help="Show DDNS status", action="store_true") - group.add_argument("--update", help="Update DDNS on a given interface", action="store_true") - args = parser.parse_args() - - # Do nothing if service is not configured - c = Config() - if not c.exists_effective('service dns dynamic'): - print("Dynamic DNS not configured") - sys.exit(1) - - if args.status: - show_status() - elif args.update: - update_ddns() diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 36bb013fe..4dcffc412 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -327,6 +327,8 @@ def show_firewall_group(name=None): dest_group = dict_search_args(rule_conf, 'destination', 'group', group_type) in_interface = dict_search_args(rule_conf, 'inbound_interface', 'group') out_interface = dict_search_args(rule_conf, 'outbound_interface', 'group') + dyn_group_source = dict_search_args(rule_conf, 'add_address_to_group', 'source_address', group_type) + dyn_group_dst = dict_search_args(rule_conf, 'add_address_to_group', 'destination_address', group_type) if source_group: if source_group[0] == "!": source_group = source_group[1:] @@ -348,6 +350,14 @@ def show_firewall_group(name=None): if group_name == out_interface: out.append(f'{item}-{name_type}-{priority}-{rule_id}') + if dyn_group_source: + if group_name == dyn_group_source: + out.append(f'{item}-{name_type}-{priority}-{rule_id}') + if dyn_group_dst: + if group_name == dyn_group_dst: + out.append(f'{item}-{name_type}-{priority}-{rule_id}') + + # Look references in route | route6 for name_type in ['route', 'route6']: if name_type not in policy: @@ -423,26 +433,37 @@ def show_firewall_group(name=None): rows = [] for group_type, group_type_conf in firewall['group'].items(): - for group_name, group_conf in group_type_conf.items(): - if name and name != group_name: - continue + ## + if group_type != 'dynamic_group': - references = find_references(group_type, group_name) - row = [group_name, group_type, '\n'.join(references) or 'N/D'] - if 'address' in group_conf: - row.append("\n".join(sorted(group_conf['address']))) - elif 'network' in group_conf: - row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) - elif 'mac_address' in group_conf: - row.append("\n".join(sorted(group_conf['mac_address']))) - elif 'port' in group_conf: - row.append("\n".join(sorted(group_conf['port']))) - elif 'interface' in group_conf: - row.append("\n".join(sorted(group_conf['interface']))) - else: - row.append('N/D') - rows.append(row) + for group_name, group_conf in group_type_conf.items(): + if name and name != group_name: + continue + references = find_references(group_type, group_name) + row = [group_name, group_type, '\n'.join(references) or 'N/D'] + if 'address' in group_conf: + row.append("\n".join(sorted(group_conf['address']))) + elif 'network' in group_conf: + row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network))) + elif 'mac_address' in group_conf: + row.append("\n".join(sorted(group_conf['mac_address']))) + elif 'port' in group_conf: + row.append("\n".join(sorted(group_conf['port']))) + elif 'interface' in group_conf: + row.append("\n".join(sorted(group_conf['interface']))) + else: + row.append('N/D') + rows.append(row) + + else: + for dynamic_type in ['address_group', 'ipv6_address_group']: + if dynamic_type in firewall['group']['dynamic_group']: + for dynamic_name, dynamic_conf in firewall['group']['dynamic_group'][dynamic_type].items(): + references = find_references(dynamic_type, dynamic_name) + row = [dynamic_name, dynamic_type + '(dynamic)', '\n'.join(references) or 'N/D'] + row.append('N/D') + rows.append(row) if rows: print('Firewall Groups\n') |