diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fe7a8b12b..cdd65d220 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "settings": { "terminal.integrated.profiles.linux": { "bash": { - "path": "bash", + "path": "bash" } } }, diff --git a/.rubocop.yml b/.rubocop.yml index 130d69f0f..f9b0b644f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -88,3 +88,6 @@ Style/Documentation: - spec/**/* Style/WordArray: EnforcedStyle: brackets +Naming/MethodParameterName: + AllowedNames: + - 'is' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 108e85171..23fae0dce 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,149 +1,53 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2023-03-29 09:22:12 UTC using RuboCop version 1.48.1. +# on 2023-08-29 15:24:31 UTC using RuboCop version 1.48.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 13 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). Layout/ClosingHeredocIndentation: Exclude: - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - - 'spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb' - - 'spec/acceptance/firewall_duplicate_comment_spec.rb' - 'spec/spec_helper_acceptance_local.rb' -# Offense count: 26 -# This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLineAfterGuardClause: - Exclude: - - 'lib/puppet/provider/firewall/ip6tables.rb' - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/provider/firewallchain/iptables_chain.rb' - - 'lib/puppet/type/firewall.rb' - - 'lib/puppet/util/firewall.rb' - - 'lib/puppet/util/ipcidr.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. -# SupportedHashRocketStyles: key, separator, table -# SupportedColonStyles: key, separator, table -# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit -Layout/HashAlignment: - Exclude: - - 'spec/unit/puppet/type/firewall_spec.rb' - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). -Layout/HeredocIndentation: - Exclude: - - 'spec/unit/puppet/type/firewallchain_spec.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: leading, trailing -Layout/LineContinuationLeadingSpace: - Exclude: - - 'lib/puppet/type/firewallchain.rb' - -# Offense count: 7 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/LineEndStringConcatenationIndentation: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/type/firewall.rb' - - 'lib/puppet/type/firewallchain.rb' - - 'spec/unit/puppet/type/firewall_spec.rb' - -# Offense count: 59 -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/BooleanSymbol: - Exclude: - - 'lib/puppet/provider/firewall/ip6tables.rb' - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/type/firewall.rb' - - 'lib/puppet/type/firewallchain.rb' - -# Offense count: 4 -# Configuration parameters: AllowedMethods. -# AllowedMethods: enums -Lint/ConstantDefinitionInBlock: - Exclude: - - 'lib/puppet/provider/firewallchain/iptables_chain.rb' - -# Offense count: 6 -# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. -Lint/DuplicateBranch: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/util/firewall.rb' - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - - 'spec/unit/puppet/type/firewallchain_spec.rb' - -# Offense count: 1 -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'spec/unit/puppet/provider/ip6tables_spec.rb' - -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). Lint/RedundantCopEnableDirective: Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - 'spec/unit/classes/firewall_spec.rb' - - 'spec/unit/puppet/type/firewall_spec.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedMethods. -# AllowedMethods: instance_of?, kind_of?, is_a?, eql?, respond_to?, equal? -Lint/RedundantSafeNavigation: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' -# Offense count: 13 +# Offense count: 20 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 235 + Max: 270 -# Offense count: 17 +# Offense count: 2 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: - Max: 64 + Max: 127 -# Offense count: 2 -# Configuration parameters: CountBlocks. -Metrics/BlockNesting: - Max: 4 +# Offense count: 3 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 776 -# Offense count: 8 +# Offense count: 17 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 60 + Max: 122 -# Offense count: 19 +# Offense count: 20 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Max: 233 + Max: 135 -# Offense count: 1 -# Configuration parameters: CountComments, CountAsOne. -Metrics/ModuleLength: - Max: 193 - -# Offense count: 6 +# Offense count: 12 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 65 + Max: 122 # Offense count: 1 # Configuration parameters: EnforcedStyleForLeadingUnderscores. @@ -152,51 +56,6 @@ Naming/MemoizedInstanceVariableName: Exclude: - 'spec/spec_helper_acceptance_local.rb' -# Offense count: 3 -# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. -# AllowedNames: as, at, by, cc, db, id, if, in, io, ip, of, on, os, pp, to -Naming/MethodParameterName: - Exclude: - - 'lib/puppet/type/firewall.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: PreferredName. -Naming/RescuedExceptionsVariableName: - Exclude: - - 'lib/puppet/util/firewall.rb' - -# Offense count: 12 -# Configuration parameters: MinSize. -Performance/CollectionLiteralInLoop: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - - 'spec/unit/puppet/type/firewall_spec.rb' - - 'spec/unit/puppet/type/firewallchain_spec.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Performance/StringIdentifierArgument: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - -# Offense count: 7 -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/StringInclude: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/type/firewall.rb' - - 'lib/puppet/util/ipcidr.rb' - -# Offense count: 15 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: be, be_nil -RSpec/BeNil: - Exclude: - - 'spec/unit/puppet/type/firewall_spec.rb' - - 'spec/unit/puppet/util/firewall_spec.rb' - # Offense count: 7 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without @@ -206,62 +65,38 @@ RSpec/ContextWording: - 'spec/unit/classes/firewall_linux_redhat_spec.rb' - 'spec/unit/classes/firewall_linux_spec.rb' -# Offense count: 25 +# Offense count: 18 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: Enabled: false -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -RSpec/EmptyHook: - Exclude: - - 'spec/unit/puppet/provider/ip6tables_spec.rb' - -# Offense count: 153 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowConsecutiveOneLiners. RSpec/EmptyLineAfterExample: Exclude: - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - - 'spec/acceptance/firewall_attributes_happy_path_spec.rb' - - 'spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb' - - 'spec/acceptance/firewall_attributes_ipv6_happy_path_spec.rb' - - 'spec/acceptance/rules_spec.rb' - 'spec/unit/classes/firewall_linux_archlinux_spec.rb' - 'spec/unit/classes/firewall_linux_debian_spec.rb' - 'spec/unit/classes/firewall_linux_redhat_spec.rb' - - 'spec/unit/puppet/provider/iptables_spec.rb' - - 'spec/unit/puppet/type/firewall_spec.rb' - - 'spec/unit/puppet/type/firewallchain_spec.rb' - - 'spec/unit/puppet/util/firewall_spec.rb' -# Offense count: 16 +# Offense count: 1 # This cop supports safe autocorrection (--autocorrect). RSpec/EmptyLineAfterExampleGroup: Exclude: - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - - 'spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb' - 'spec/unit/classes/firewall_spec.rb' - - 'spec/unit/puppet/provider/ip6tables_spec.rb' - - 'spec/unit/puppet/type/firewall_spec.rb' - - 'spec/unit/puppet/type/firewallchain_spec.rb' -# Offense count: 22 +# Offense count: 7 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowConsecutiveOneLiners. RSpec/EmptyLineAfterHook: Exclude: - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - - 'spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb' - - 'spec/acceptance/firewall_attributes_ipv6_happy_path_spec.rb' - 'spec/unit/facter/iptables_persistent_version_spec.rb' - 'spec/unit/facter/iptables_spec.rb' - - 'spec/unit/puppet/type/firewallchain_spec.rb' -# Offense count: 22 +# Offense count: 9 # Configuration parameters: CountAsOne. RSpec/ExampleLength: - Max: 16 + Max: 8 # Offense count: 41 # This cop supports safe autocorrection (--autocorrect). @@ -273,16 +108,16 @@ RSpec/ImplicitSubject: - 'spec/unit/classes/firewall_linux_debian_spec.rb' - 'spec/unit/classes/firewall_linux_redhat_spec.rb' -# Offense count: 44 +# Offense count: 41 RSpec/MultipleExpectations: - Max: 6 + Max: 2 -# Offense count: 16 +# Offense count: 38 # Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 5 -# Offense count: 66 +# Offense count: 43 # Configuration parameters: AllowedPatterns. # AllowedPatterns: ^expect_, ^assert_ RSpec/NoExpectationExample: @@ -293,213 +128,58 @@ RSpec/NoExpectationExample: - 'spec/acceptance/firewallchain_spec.rb' - 'spec/acceptance/rules_spec.rb' - 'spec/acceptance/standard_usage_spec.rb' - - 'spec/unit/puppet/provider/ip6tables_spec.rb' - - 'spec/unit/puppet/provider/iptables_chain_spec.rb' - - 'spec/unit/puppet/provider/iptables_spec.rb' - - 'spec/unit/puppet/type/firewall_spec.rb' - - 'spec/unit/puppet/type/firewallchain_spec.rb' - - 'spec/unit/puppet/util/firewall_spec.rb' - - 'spec/unit/puppet/util/ipcidr_spec.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -RSpec/ReceiveNever: - Exclude: - - 'spec/unit/puppet/util/firewall_spec.rb' # Offense count: 3 RSpec/RepeatedExampleGroupBody: Exclude: - 'spec/unit/classes/firewall_linux_debian_spec.rb' -# Offense count: 8 -RSpec/RepeatedExampleGroupDescription: - Exclude: - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - - 'spec/acceptance/resource_cmd_spec.rb' - -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: . -# SupportedStyles: constant, string -RSpec/VerifiedDoubleReference: - EnforcedStyle: string - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: nested, compact -Style/ClassAndModuleChildren: - Exclude: - - 'lib/puppet/util/ipcidr.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/CollectionCompact: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - -# Offense count: 1 -Style/CombinableLoops: - Exclude: - - 'spec/unit/puppet/type/firewall_spec.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ConcatArrayLiterals: +# Offense count: 83 +# Configuration parameters: AllowedVariables. +Style/GlobalVars: Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' + - 'lib/puppet/provider/firewall/firewall.rb' + - 'lib/puppet/provider/firewallchain/firewallchain.rb' -# Offense count: 90 +# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). Style/IfUnlessModifier: Exclude: - - 'lib/puppet/provider/firewall.rb' - - 'lib/puppet/provider/firewall/ip6tables.rb' - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/provider/firewallchain/iptables_chain.rb' - - 'lib/puppet/type/firewall.rb' - - 'lib/puppet/type/firewallchain.rb' - - 'lib/puppet/util/firewall.rb' - - 'spec/acceptance/class_spec.rb' - - 'spec/acceptance/firewall_attributes_happy_path_spec.rb' - - 'spec/acceptance/firewall_duplicate_comment_spec.rb' - - 'spec/acceptance/rules_spec.rb' - 'spec/spec_helper_acceptance_local.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedMethods. -# AllowedMethods: nonzero? -Style/IfWithBooleanLiteralBranches: - Exclude: - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - # Offense count: 1 Style/MixinUsage: Exclude: - 'spec/spec_helper.rb' # Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/NegatedIfElseCondition: - Exclude: - - 'lib/puppet/type/firewallchain.rb' - -# Offense count: 9 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Exclude: - - 'spec/**/*' - - 'lib/puppet/provider/firewall/ip6tables.rb' - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/type/firewall.rb' - - 'lib/puppet/util/firewall.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: same_as_string_literals, single_quotes, double_quotes -Style/QuotedSymbols: - Exclude: - - 'lib/puppet/type/firewall.rb' - -# Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Methods. Style/RedundantArgument: Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - 'spec/spec_helper_acceptance_local.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantAssignment: - Exclude: - - 'spec/acceptance/firewall_duplicate_comment_spec.rb' - # Offense count: 2 # This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: - Exclude: - - 'lib/puppet/type/firewall.rb' - -# Offense count: 113 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpEscape: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/type/firewall.rb' - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - - 'spec/acceptance/firewall_attributes_happy_path_spec.rb' - - 'spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb' - - 'spec/acceptance/firewall_attributes_ipv6_happy_path_spec.rb' - - 'spec/acceptance/rules_spec.rb' - - 'spec/unit/puppet/provider/iptables_spec.rb' - - 'spec/unit/puppet/type/firewall_spec.rb' - -# Offense count: 8 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantStringEscape: - Exclude: - - 'spec/acceptance/firewall_attributes_exceptions_spec.rb' - - 'spec/acceptance/resource_cmd_spec.rb' - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, explicit Style/RescueStandardError: Exclude: - - 'lib/puppet/util/firewall.rb' - 'spec/spec_helper.rb' - 'spec/spec_helper_acceptance_local.rb' -# Offense count: 2 +# Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - 'spec/spec_helper_acceptance_local.rb' -# Offense count: 16 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowModifier. -Style/SoleNestedConditional: - Exclude: - - 'lib/puppet/type/firewall.rb' - -# Offense count: 6 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Mode. -Style/StringConcatenation: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/provider/firewallchain/iptables_chain.rb' - - 'lib/puppet/type/firewall.rb' - - 'lib/puppet/util/firewall.rb' - - 'spec/unit/puppet/type/firewallchain_spec.rb' - -# Offense count: 4 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. -# AllowedMethods: define_method -Style/SymbolProc: - Exclude: - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/type/firewall.rb' - -# Offense count: 46 +# Offense count: 33 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyleForMultiline. # SupportedStylesForMultiline: comma, consistent_comma, no_comma Style/TrailingCommaInHashLiteral: Exclude: - - 'lib/puppet/provider/firewall/ip6tables.rb' - - 'lib/puppet/provider/firewall/iptables.rb' - - 'lib/puppet/provider/firewallchain/iptables_chain.rb' - 'spec/spec_helper.rb' - 'spec/spec_helper_local.rb' - 'spec/unit/classes/firewall_linux_archlinux_spec.rb' @@ -507,4 +187,3 @@ Style/TrailingCommaInHashLiteral: - 'spec/unit/classes/firewall_linux_redhat_spec.rb' - 'spec/unit/classes/firewall_linux_spec.rb' - 'spec/unit/facter/iptables_persistent_version_spec.rb' - - 'spec/unit/puppet/type/firewall_spec.rb' diff --git a/.sync.yml b/.sync.yml index 5762116f8..daa8e5e47 100644 --- a/.sync.yml +++ b/.sync.yml @@ -6,6 +6,7 @@ appveyor.yml: Gemfile: optional: ":development": + - gem: 'puppet-resource_api' - gem: github_changelog_generator version: '= 1.15.2' Rakefile: diff --git a/Gemfile b/Gemfile index 6235e42e1..93caf7f03 100644 --- a/Gemfile +++ b/Gemfile @@ -34,6 +34,7 @@ group :development do gem "rubocop-performance", '= 1.16.0', require: false gem "rubocop-rspec", '= 2.19.0', require: false gem "rb-readline", '= 0.5.5', require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "puppet-resource_api", require: false gem "github_changelog_generator", '= 1.15.2', require: false end group :system_tests do diff --git a/README.md b/README.md index b321070aa..5c24018eb 100644 --- a/README.md +++ b/README.md @@ -170,15 +170,7 @@ resources { 'firewallchain': Internal chains can not be deleted. In order to avoid all the confusing Warning/Notice messages when using `purge => true`, like these ones: - Notice: Compiled catalog for blonde-height.delivery.puppetlabs.net in environment production in 0.05 seconds - Warning: Firewallchain[INPUT:mangle:IPv4](provider=iptables_chain): Attempting to destroy internal chain INPUT:mangle:IPv4 - Notice: /Stage[main]/Main/Firewallchain[INPUT:mangle:IPv4]/ensure: removed - Warning: Firewallchain[FORWARD:mangle:IPv4](provider=iptables_chain): Attempting to destroy internal chain FORWARD:mangle:IPv4 - Notice: /Stage[main]/Main/Firewallchain[FORWARD:mangle:IPv4]/ensure: removed - Warning: Firewallchain[OUTPUT:mangle:IPv4](provider=iptables_chain): Attempting to destroy internal chain OUTPUT:mangle:IPv4 - Notice: /Stage[main]/Main/Firewallchain[OUTPUT:mangle:IPv4]/ensure: removed - Warning: Firewallchain[POSTROUTING:mangle:IPv4](provider=iptables_chain): Attempting to destroy internal chain POSTROUTING:mangle:IPv4 - Notice: /Stage[main]/Main/Firewallchain[POSTROUTING:mangle:IPv4]/ensure: removed + Warning: Inbuilt Chains may not be deleted. Chain `POSTROUTING:mangle:IPv6` will be flushed and have it's policy reverted to default. Please create firewallchains for every internal chain. Here is an example: @@ -248,7 +240,7 @@ firewall { '006 Allow inbound SSH (v6)': dport => 22, proto => 'tcp', action => 'accept', - provider => 'ip6tables', + protocol => 'ip6tables', } ``` @@ -280,10 +272,13 @@ class profile::apache { ### Rule inversion -Firewall rules may be inverted by prefixing the value of a parameter by "! ". If the value is an array, then every item in the array must be prefixed as iptables does not understand inverting a single value. +Firewall rules may be inverted by prefixing the value of a parameter by "! ". Parameters that understand inversion are: connmark, ctstate, destination, dport, dst\_range, dst\_type, iniface, outiface, port, proto, source, sport, src\_range and src\_type. +If the value is an array, then either the first value of the array, or all of its values must be prefixed in order to invert them all. +For most array attributes it is not possible to invert only one passed value. + Examples: ```puppet @@ -297,12 +292,23 @@ firewall { '002 drop NEW external website packets with FIN/RST/ACK set and SYN u state => 'NEW', action => 'drop', proto => 'tcp', - sport => ['! http', '! 443'], + sport => ['! http', '443'], source => '! 10.0.0.0/8', tcp_flags => '! FIN,SYN,RST,ACK SYN', } ``` +There are exceptions to this however, with attributes such as src\_type, dst\_type and ipset allowing the user to negate each passed values seperately. + +Examples: + +```puppet +firewall { '001 allow local disallow anycast': + action => 'accept', + src_type => ['LOCAL', '! ANYCAST'], +} +``` + ### Additional uses for the firewall module You can apply firewall rules to specific nodes. Usually, you should put the firewall rule in another class and apply that class to a node. Apply a rule to a node as follows: @@ -542,3 +548,61 @@ And run the tests from the root of the source code: ```text bundle exec rake parallel_spec ``` + +See the Github Action runs for information on running the acceptance and other tests. + +### Migration path to v7.0.0 + +As of `v7.0.0` of this module a major rework has been done to adopt the [puppet-resource_api](https://github.com/puppetlabs/puppet-resource_api) into the module and use it style of code in place of the original form of Puppet Type and Providers. This was done in the most part to increase the ease with with the module could be maintained and updated in the future, the changes helping to structure the module in such a way as to be more easily understood and altered going forward. + +As part of this process several breaking changes where made to the code that will need to be accounted for whenever you update to this new version of the module, with these changes including: + +* The `provider` attibute within the `firewall` type has been renamed to `protocol`, both to bring it in line with the matching attribute within the `firewallchain` type and due to the resource_api forbidding the use of `provider` as a attribute name. As part of this the attribute has also been updated to accept `IPv4` and `IPv6` in place of `iptables` or `ip6tables`, though they are still valid as input. +* The `action` attribute within the `firewall` type has been removed as it was merely a restricted version of the `jump` attribute, both of them managing the same function, this being reasoned as a way to enforce the use of generic parameters. From this point the parameters formerly unique to `action` should now be passed to `jump`. +* Strict types have now been implemented for all attributes, while this should not require changes on the user end in most cases, there may be some instances where manifests will require updated to match the new expected form of input. +* Attributes that allow both arrays and negated values have now been updated. + * For attributes that require that all passed values be negated as one, you now merely have to negate the first value within the array, rather than all of them, though negating all is still accepted. + * For attributes that allow passed values to be negated seperately this is not the case. All attributes in this situation are noted within their description. +* The `sport` and `dport` attributes have been updated so that they will now accept with `:` or `-` as a separator when passing ranges, with `:` being preferred as it matchs what is passed to iptables. + +Two pairs of manifest tkane from the tests can be seen below, illustrating the changes that may be required, the fist applying a hoplimit on `ip6tables`: + +```Puppet +firewall { '571 - hop_limit': + ensure => present, + proto => 'tcp', + dport => '571', + action => 'accept', + hop_limit => '5', + provider => 'ip6tables', +} +``` + +```Puppet +firewall { '571 - hop_limit': + ensure => present, + proto => 'tcp', + dport => '571', + jump => 'accept', + hop_limit => '5', + protocol => 'IPv6', +} +``` + +And the second negating access to a range of ports on `iptables`: + +```puppet +firewall { '560 - negated ports': + proto => `tcp`, + sport => ['! 560-570','! 580'], + action => `accept`, +} +``` + +```puppet +firewall { '560 - negated ports': + proto => `tcp`, + sport => '! 560:570','580', + jump => `accept`, +} +``` diff --git a/REFERENCE.md b/REFERENCE.md index 004605396..6dd126488 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -21,8 +21,8 @@ ### Resource types -* [`firewall`](#firewall): This type provides the capability to manage firewall rules within puppet. -* [`firewallchain`](#firewallchain): This type provides the capability to manage rule chains for firewalls. +* [`firewall`](#firewall): This type provides the capability to manage firewall rules within puppet via iptables. **Autorequires:** If Puppet is managing the iptables +* [`firewallchain`](#firewallchain): This type provides the capability to manage rule chains for firewalls. Currently this supports only iptables, ip6tables and ebtables on Linu ## Classes @@ -115,32 +115,19 @@ Default value: `false` ### `firewall` +This type provides the capability to manage firewall rules within puppet via iptables. + **Autorequires:** -If Puppet is managing the iptables or ip6tables chains specified in the +If Puppet is managing the iptables chains specified in the `chain` or `jump` parameters, the firewall resource will autorequire those firewallchain resources. If Puppet is managing the iptables, iptables-persistent, or iptables-services packages, -and the provider is iptables or ip6tables, the firewall resource will -autorequire those packages to ensure that any required binaries are +the firewall resource will autorequire those packages to ensure that any required binaries are installed. #### Providers - Note: Not all features are available with all providers. - - * ip6tables: Ip6tables type provider - - * Required binaries: ip6tables-save, ip6tables. - * Supported features: address_type, connection_limiting, conntrack, dnat, hop_limiting, icmp_match, - interface_match, iprange, ipsec_dir, ipsec_policy, ipset, iptables, isfirstfrag, - ishasmorefrags, islastfrag, length, log_level, log_prefix, log_uid, - log_tcp_sequence, log_tcp_options, log_ip_options, mask, mss, - owner, pkttype, queue_bypass, queue_num, rate_limiting, recent_limiting, reject_type, - snat, socket, state_match, string_matching, tcp_flags, hashlimit, bpf. - - * iptables: Iptables type provider - * Required binaries: iptables-save, iptables. * Default for kernel == linux. * Supported features: address_type, clusterip, connection_limiting, conntrack, dnat, icmp_match, @@ -249,1176 +236,1489 @@ installed. The following properties are available in the `firewall` type. -##### `action` - -Valid values: `accept`, `reject`, `drop` - -This is the action to perform on a match. Can be one of: - -* accept - the packet is accepted -* reject - the packet is rejected with a suitable ICMP response -* drop - the packet is dropped - -If you specify no value it will simply match the rule but perform no -action unless you provide a provider specific parameter (such as *jump*). - ##### `burst` -Valid values: `%r{^\d+$}` +Data type: `Optional[Integer[1]]` -Rate limiting burst value (per second) before limit checks apply. + Rate limiting burst value (per second) before limit checks apply. ##### `bytecode` -Match using Linux Socket Filter. Expects a BPF program in decimal format. -This is the format generated by the nfbpf_compile utility. +Data type: `Optional[String[1]]` + + Match using Linux Socket Filter. Expects a BPF program in decimal format. + This is the format generated by the nfbpf_compile utility. ##### `cgroup` -Matches against the net_cls cgroup ID of the packet. +Data type: `Optional[String[1]]` + + Matches against the net_cls cgroup ID of the packet. + + To negate add a space seperate `!` to the beginning of the string ##### `chain` -Valid values: `%r{^[a-zA-Z0-9\-_]+$}` +Data type: `String[1]` -Name of the chain to use. Can be one of the built-ins: + Name of the chain the rule will be a part of, ensure the chain you choose exists within your set table. + Can be one of the built-in chains: -* INPUT -* FORWARD -* OUTPUT -* PREROUTING -* POSTROUTING + * INPUT + * FORWARD + * OUTPUT + * PREROUTING + * POSTROUTING -Or you can provide a user-based chain. + Or you can provide a user-based chain. + Defaults to 'INPUT' Default value: `INPUT` ##### `checksum_fill` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Compute and fill missing packet checksums. + Compute and fill missing packet checksums. ##### `clamp_mss_to_pmtu` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Sets the clamp mss to pmtu flag. + Sets the clamp mss to pmtu flag. ##### `clusterip_clustermac` -Valid values: `%r{^([0-9a-f]{2}[:]){5}([0-9a-f]{2})$}i` +Data type: `Optional[Pattern[/^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -Used with the CLUSTERIP jump target. -Specify the ClusterIP MAC address. Has to be a link-layer multicast address. + Used with the CLUSTERIP jump target. + Specify the ClusterIP MAC address. Has to be a link-layer multicast address. + This is IPv4 specific. ##### `clusterip_hash_init` -Used with the CLUSTERIP jump target. -Specify the random seed used for hash initialization. +Data type: `Optional[String[1]]` + + Used with the CLUSTERIP jump target. + Specify the random seed used for hash initialization. + This is IPv4 specific. ##### `clusterip_hashmode` -Valid values: `sourceip`, `sourceip-sourceport`, `sourceip-sourceport-destport` +Data type: `Optional[Enum['sourceip', 'sourceip-sourceport', 'sourceip-sourceport-destport']]` -Used with the CLUSTERIP jump target. -Specify the hashing mode. + Used with the CLUSTERIP jump target. + Specify the hashing mode. + This is IPv4 specific. ##### `clusterip_local_node` -Valid values: `%r{\d+}` +Data type: `Optional[Integer[1]]` -Used with the CLUSTERIP jump target. -Specify the random seed used for hash initialization. + Used with the CLUSTERIP jump target. + Specify the random seed used for hash initialization. + This is IPv4 specific. ##### `clusterip_new` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Used with the CLUSTERIP jump target. -Create a new ClusterIP. You always have to set this on the first rule for a given ClusterIP. + Used with the CLUSTERIP jump target. + Create a new ClusterIP. You always have to set this on the first rule for a given ClusterIP. + This is IPv4 specific. ##### `clusterip_total_nodes` -Valid values: `%r{\d+}` +Data type: `Optional[Integer[1]]` -Used with the CLUSTERIP jump target. -Number of total nodes within this cluster. + Used with the CLUSTERIP jump target. + Number of total nodes within this cluster. + This is IPv4 specific. ##### `condition` -Match on boolean value (0/1) stored in /proc/net/nf_condition/name. +Data type: `Optional[String[1]]` + + Match on boolean value (0/1) stored in /proc/net/nf_condition/name. ##### `connlimit_above` -Valid values: `%r{^\d+$}` +Data type: `Optional[Integer]` -Connection limiting value for matched connections above n. + Connection limiting value for matched connections above n. ##### `connlimit_mask` -Valid values: `%r{^\d+$}` +Data type: `Optional[Integer[0,128]]` + + Connection limiting by subnet mask for matched connections. + IPv4: 0-32 + IPv6: 0-128 + +##### `connlimit_upto` -Connection limiting by subnet mask for matched connections. -IPv4: 0-32 -IPv6: 0-128 +Data type: `Optional[Integer]` + + Connection limiting value for matched connections below or equal to n. ##### `connmark` -Match the Netfilter mark value associated with the packet. Accepts either of: -mark/mask or mark. These will be converted to hex if they are not already. +Data type: `Optional[Pattern[/^(?:!\s)?[a-fA-F0-9x]+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Match the Netfilter mark value associated with the packet, accepts a mark. + This value will be converted to hex if it is not already. + This value can be negated by adding a space seperated `!` to the beginning. ##### `ctdir` -Valid values: `REPLY`, `ORIGINAL` +Data type: `Optional[Enum['REPLY', 'ORIGINAL']]` -Matches a packet that is flowing in the specified direction using the -conntrack module. If this flag is not specified at all, matches packets -in both directions. Values can be: + Matches a packet that is flowing in the specified direction using the + conntrack module. If this flag is not specified at all, matches packets + in both directions. Values can be: -* REPLY -* ORIGINAL + * REPLY + * ORIGINAL ##### `ctexpire` -Valid values: `%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}` +Data type: `Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -Matches a packet based on lifetime remaining in seconds or range of values -using the conntrack module. For example: + Matches a packet based on lifetime remaining in seconds or range of seconds + using the conntrack module. For example: - ctexpire => '100:150' + ctexpire => '100' + ctexpire => '100:150' ##### `ctorigdst` -The original destination address using the conntrack module. For example: +Data type: `Optional[String[1]]` - ctorigdst => '192.168.2.0/24' + The original destination address using the conntrack module. For example: -You can also negate a mask by putting ! in front. For example: + ctorigdst => '192.168.2.0/24' - ctorigdst => '! 192.168.2.0/24' + You can also negate a mask by putting ! in front. For example: -The ctorigdst can also be an IPv6 address if your provider supports it. + ctorigdst => '! 192.168.2.0/24' + + The ctorigdst can also be an IPv6 address if your provider supports it. ##### `ctorigdstport` -Valid values: `%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}` +Data type: `Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -The original destination port to match for this filter using the conntrack module. -For example: + The original destination port to match for this filter using the conntrack module. + For example: - ctorigdstport => '80' + ctorigdstport => '80' -You can also specify a port range: For example: + You can also specify a port range: For example: - ctorigdstport => '80:81' + ctorigdstport => '80:81' -You can also negate a port by putting ! in front. For example: + You can also negate a port by putting ! in front. For example: - ctorigdstport => '! 80' + ctorigdstport => '! 80' ##### `ctorigsrc` -The original source address using the conntrack module. For example: +Data type: `Optional[String[1]]` + + The original source address using the conntrack module. For example: - ctorigsrc => '192.168.2.0/24' + ctorigsrc => '192.168.2.0/24' -You can also negate a mask by putting ! in front. For example: + You can also negate a mask by putting ! in front. For example: - ctorigsrc => '! 192.168.2.0/24' + ctorigsrc => '! 192.168.2.0/24' -The ctorigsrc can also be an IPv6 address if your provider supports it. + The ctorigsrc can also be an IPv6 address if your provider supports it. ##### `ctorigsrcport` -Valid values: `%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}` +Data type: `Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -The original source port to match for this filter using the conntrack module. -For example: + The original source port to match for this filter using the conntrack module. + For example: - ctorigsrcport => '80' + ctorigsrcport => '80' -You can also specify a port range: For example: + You can also specify a port range: For example: - ctorigsrcport => '80:81' + ctorigsrcport => '80:81' -You can also negate a port by putting ! in front. For example: + You can also negate a port by putting ! in front. For example: - ctorigsrcport => '! 80' + ctorigsrcport => '! 80' ##### `ctproto` -Valid values: `%r{^!?\s?\d+$}` +Data type: `Optional[Variant[Pattern[/^(?:!\s)?\d+$/],Integer]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -The specific layer-4 protocol number to match for this rule using the -conntrack module. + The specific layer-4 protocol number to match for this rule using the + conntrack module. ##### `ctrepldst` -The reply destination address using the conntrack module. For example: +Data type: `Optional[String[1]]` + + The reply destination address using the conntrack module. For example: - ctrepldst => '192.168.2.0/24' + ctrepldst => '192.168.2.0/24' -You can also negate a mask by putting ! in front. For example: + You can also negate a mask by putting ! in front. For example: - ctrepldst => '! 192.168.2.0/24' + ctrepldst => '! 192.168.2.0/24' -The ctrepldst can also be an IPv6 address if your provider supports it. + The ctrepldst can also be an IPv6 address if your provider supports it. ##### `ctrepldstport` -Valid values: `%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}` +Data type: `Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -The reply destination port to match for this filter using the conntrack module. -For example: + The reply destination port to match for this filter using the conntrack module. + For example: - ctrepldstport => '80' + ctrepldstport => '80' -You can also specify a port range: For example: + You can also specify a port range: For example: - ctrepldstport => '80:81' + ctrepldstport => '80:81' -You can also negate a port by putting ! in front. For example: + You can also negate a port by putting ! in front. For example: - ctrepldstport => '! 80' + ctrepldstport => '! 80' ##### `ctreplsrc` -The reply source address using the conntrack module. For example: +Data type: `Optional[String[1]]` + + The reply source address using the conntrack module. For example: - ctreplsrc => '192.168.2.0/24' + ctreplsrc => '192.168.2.0/24' -You can also negate a mask by putting ! in front. For example: + You can also negate a mask by putting ! in front. For example: - ctreplsrc => '! 192.168.2.0/24' + ctreplsrc => '! 192.168.2.0/24' -The ctreplsrc can also be an IPv6 address if your provider supports it. + The ctreplsrc can also be an IPv6 address if your provider supports it. ##### `ctreplsrcport` -Valid values: `%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}` +Data type: `Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -The reply source port to match for this filter using the conntrack module. -For example: + The reply source port to match for this filter using the conntrack module. + For example: - ctreplsrcport => '80' + ctreplsrcport => '80' -You can also specify a port range: For example: + You can also specify a port range: For example: - ctreplsrcport => '80:81' + ctreplsrcport => '80:81' -You can also negate a port by putting ! in front. For example: + You can also negate a port by putting ! in front. For example: - ctreplsrcport => '! 80' + ctreplsrcport => '! 80' ##### `ctstate` -Valid values: `INVALID`, `ESTABLISHED`, `NEW`, `RELATED`, `UNTRACKED`, `SNAT`, `DNAT` +Data type: `Optional[Variant[Pattern[/^(?:!\s)?(?:INVALID|ESTABLISHED|NEW|RELATED|UNTRACKED|SNAT|DNAT)$/], Array[Pattern[/^(?:!\s)?(?:INVALID|ESTABLISHED|NEW|RELATED|UNTRACKED|SNAT|DNAT)$/]]]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Matches a packet based on its state in the firewall stateful inspection + table, using the conntrack module. Values can be: + + * INVALID + * ESTABLISHED + * NEW + * RELATED + * UNTRACKED + * SNAT + * DNAT + + Can be passed either as a single String or as an Array, if passed as an array values should be passed in order: + + ctstate => 'INVALID' + ctstate => ['INVALID', 'ESTABLISHED'] + + Values can be negated by adding a '!'. + If you wish to negate multiple states at once, then place a ! at the start of the first array + variable. For example: -Matches a packet based on its state in the firewall stateful inspection -table, using the conntrack module. Values can be: + ctstate => ['! INVALID', 'ESTABLISHED'] -* INVALID -* ESTABLISHED -* NEW -* RELATED -* UNTRACKED -* SNAT -* DNAT + Note: + This will negate all passed states, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. ##### `ctstatus` -Valid values: `NONE`, `EXPECTED`, `SEEN_REPLY`, `ASSURED`, `CONFIRMED` +Data type: `Optional[Variant[Pattern[/^(?:!\s)?(?:EXPECTED|SEEN_REPLY|ASSURED|CONFIRMED|NONE)$/], Array[Pattern[/^(?:!\s)?(?:EXPECTED|SEEN_REPLY|ASSURED|CONFIRMED|NONE)$/]]]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -Matches a packet based on its status using the conntrack module. Values can be: + Matches a packet based on its status using the conntrack module. Values can be: -* EXPECTED -* SEEN_REPLY -* ASSURED -* CONFIRMED + * EXPECTED + * SEEN_REPLY + * ASSURED + * CONFIRMED + * NONE + + Can be passed either as a single String or as an Array: + + ctstatus => 'EXPECTED' + ctstatus => ['EXPECTED', 'CONFIRMED'] + + Values can be negated by adding a '!'. + If you wish to negate multiple states at once, then place a ! at the start of the first array + variable. For example: + + ctstatus => ['! EXPECTED', 'CONFIRMED'] + + Note:#{' '} + This will negate all passed states, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. ##### `date_start` -Only match during the given time, which must be in ISO 8601 "T" notation. -The possible time range is 1970-01-01T00:00:00 to 2038-01-19T04:17:07 +Data type: `Optional[Pattern[/^[0-9]{4}\-(?:0[0-9]|1[0-2])\-(?:[0-2][0-9]|3[0-1])T(?:[0-1][0-9]|2[0-3])\:[0-5][0-9]\:[0-5][0-9]$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Only match during the given time, which must be in ISO 8601 "T" notation. + The possible time range is 1970-01-01T00:00:00 to 2038-01-19T04:17:07 ##### `date_stop` -Only match during the given time, which must be in ISO 8601 "T" notation. -The possible time range is 1970-01-01T00:00:00 to 2038-01-19T04:17:07 +Data type: `Optional[Pattern[/^[0-9]{4}\-(?:0[0-9]|1[0-2])\-(?:[0-2][0-9]|3[0-1])T(?:[0-1][0-9]|2[0-3])\:[0-5][0-9]\:[0-5][0-9]$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Only match during the given time, which must be in ISO 8601 "T" notation. + The possible time range is 1970-01-01T00:00:00 to 2038-01-19T04:17:07 ##### `destination` -The destination address to match. For example: +Data type: `Optional[String[1]]` + + The destination address to match. For example: - destination => '192.168.1.0/24' + destination => '192.168.1.0/24' -You can also negate a mask by putting ! in front. For example: + You can also negate a mask by putting ! in front. For example: - destination => '! 192.168.2.0/24' + destination => '! 192.168.2.0/24' -The destination can also be an IPv6 address if your provider supports it. + The destination can also be an IPv6 address if your provider supports it. ##### `dport` -The destination port to match for this filter (if the protocol supports -ports). Will accept a single element or an array. +Data type: `Optional[Variant[Array[Variant[Pattern[/^(?:!\s)?\d+(?:(?:\:|-)\d+)?$/],Integer]],Pattern[/^(?:!\s)?\d+(?:(?:\:|-)\d+)?$/],Integer]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + The source port to match for this filter (if the protocol supports + ports). Will accept a single element or an array. + + For some firewall providers you can pass a range of ports in the format: -For some firewall providers you can pass a range of ports in the format: + dport => '1:1024' - - + This would cover ports 1 to 1024. -For example: + You can also negate a port by putting ! in front. For example: - 1-1024 + dport => '! 54' -This would cover ports 1 to 1024. + If you wish to negate multiple ports at once, then place a ! at the start of the first array + variable. For example: + + dport => ['! 54','23'] + + Note: + This will negate all passed ports, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. ##### `dst_cc` -Valid values: `%r{^[A-Z]{2}(,[A-Z]{2})*$}` +Data type: `Optional[Pattern[/^[A-Z]{2}(,[A-Z]{2})*$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -dst attribute for the module geoip + dst attribute for the module geoip ##### `dst_range` -The destination IP range. For example: +Data type: `Optional[String[1]]` + + The destination IP range. For example: + + dst_range => '192.168.1.1-192.168.1.10' + + You can also negate the range by putting ! in front. For example: - dst_range => '192.168.1.1-192.168.1.10' + dst_range => '! 192.168.1.1-192.168.1.10' -The destination IP range must be in 'IP1-IP2' format. + The destination IP range must be in 'IP1-IP2' format. ##### `dst_type` -Valid values: `[:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, - :BLACKHOLE, :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].map { |address_type| - [ - address_type, - "! #{address_type}".to_sym, - "#{address_type} --limit-iface-in".to_sym, - "#{address_type} --limit-iface-out".to_sym, - "! #{address_type} --limit-iface-in".to_sym, - "! #{address_type} --limit-iface-out".to_sym, - ] - }.flatten` +Data type: `Optional[Variant[ + Array[Pattern[/^(?:!\s)?(?:UNSPEC|UNICAST|LOCAL|BROADCAST|ANYCAST|MULTICAST|BLACKHOLE|UNREACHABLE|UNREACHABLE|PROHIBIT|THROW|NAT|XRESOLVE)(?:\s--limit-iface-(?:in|out))?$/]], + Pattern[/^(?:!\s)?(?:UNSPEC|UNICAST|LOCAL|BROADCAST|ANYCAST|MULTICAST|BLACKHOLE|UNREACHABLE|UNREACHABLE|PROHIBIT|THROW|NAT|XRESOLVE)(?:\s--limit-iface-(?:in|out))?$/]]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -The destination address type. For example: + The destination address type. For example: - dst_type => ['LOCAL'] + dst_type => ['LOCAL'] -Can be one of: + Can be one of: -* UNSPEC - an unspecified address -* UNICAST - a unicast address -* LOCAL - a local address -* BROADCAST - a broadcast address -* ANYCAST - an anycast packet -* MULTICAST - a multicast address -* BLACKHOLE - a blackhole address -* UNREACHABLE - an unreachable address -* PROHIBIT - a prohibited address -* THROW - undocumented -* NAT - undocumented -* XRESOLVE - undocumented + * UNSPEC - an unspecified address + * UNICAST - a unicast address + * LOCAL - a local address + * BROADCAST - a broadcast address + * ANYCAST - an anycast packet + * MULTICAST - a multicast address + * BLACKHOLE - a blackhole address + * UNREACHABLE - an unreachable address + * PROHIBIT - a prohibited address + * THROW - undocumented + * NAT - undocumented + * XRESOLVE - undocumented -In addition, it accepts '--limit-iface-in' and '--limit-iface-out' flags, specified as: + In addition, it accepts '--limit-iface-in' and '--limit-iface-out' flags, specified as: - dst_type => ['LOCAL --limit-iface-in'] + dst_type => ['LOCAL --limit-iface-in'] -It can also be negated using '!': + Each value can be negated seperately using '!': - dst_type => ['! LOCAL'] + dst_type => ['! UNICAST', '! LOCAL'] -Will accept a single element or an array. + Will accept a single element or an array. ##### `ensure` -Valid values: `present`, `absent` +Data type: `Enum[present, absent, 'present', 'absent']` -Manage the state of this rule. + Whether this rule should be present or absent on the target system. Default value: `present` ##### `gateway` -The TEE target will clone a packet and redirect this clone to another -machine on the local network segment. gateway is the target host's IP. +Data type: `Optional[Pattern[/^(\d+.\d+.\d+.\d+|\w+:\w+::\w+)$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + The TEE target will clone a packet and redirect this clone to another + machine on the local network segment. + Gateway is the target host's IP. ##### `gid` -GID or Group owner matching rule. Accepts a string argument -only, as iptables does not accept multiple gid in a single -statement. +Data type: `Optional[Variant[String[1], Integer]]` + + GID or Group owner matching rule. Accepts a single argument + only, as iptables does not accept multiple gid in a single + statement. + To negate add a space seperated '!' in front of the value. ##### `goto` -The value for the iptables --goto parameter. Normal values are: +Data type: `Optional[Pattern[/^[a-zA-Z0-9_]+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + The value for the iptables --goto parameter. Normal values are: -* QUEUE -* RETURN -* DNAT -* SNAT -* LOG -* MASQUERADE -* REDIRECT -* MARK + * QUEUE + * RETURN + * DNAT + * SNAT + * LOG + * MASQUERADE + * REDIRECT + * MARK -But any valid chain name is allowed. + But any valid chain name is allowed. ##### `hashlimit_above` -Match if the rate is above amount/quantum. -This parameter or hashlimit_upto is required. -Allowed forms are '40','40/second','40/minute','40/hour','40/day'. +Data type: `Optional[Pattern[/^\d+(?:\/(?:sec|min|hour|day))?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Match if the rate is above amount/quantum. + This parameter or `hashlimit_upto` and `hashlimit_name` are required when setting any other hashlimit values. + Allowed forms are '40','40/sec','40/min','40/hour','40/day'. ##### `hashlimit_burst` -Valid values: `%r{^\d+$}` +Data type: `Optional[Integer[1]]` -Maximum initial number of packets to match: this number gets recharged by one every time the limit specified above is not reached, up to this number; the default is 5. When byte-based rate matching is requested, this option specifies the amount of bytes that can exceed the given rate. This option should be used with caution -- if the entry expires, the burst value is reset too. + Maximum initial number of packets to match: this number gets recharged by one every time the limit specified above is not reached, up to this number; the default is 5. + When byte-based rate matching is requested, this option specifies the amount of bytes that can exceed the given rate. + This option should be used with caution -- if the entry expires, the burst value is reset too. ##### `hashlimit_dstmask` -Like --hashlimit-srcmask, but for destination addresses. +Data type: `Optional[Integer[0,32]]` + + When --hashlimit-mode srcip is used, all destination addresses encountered will be grouped according to the given prefix length + and the so-created subnet will be subject to hashlimit. + Prefix must be between (inclusive) 0 and 32. + Note that --hashlimit-dstmask 0 is basically doing the same thing as not specifying srcip for --hashlimit-mode, but is technically more expensive. ##### `hashlimit_htable_expire` -After how many milliseconds do hash entries expire. +Data type: `Optional[Integer]` + + After how many milliseconds do hash entries expire. ##### `hashlimit_htable_gcinterval` -How many milliseconds between garbage collection intervals. +Data type: `Optional[Integer]` + + How many milliseconds between garbage collection intervals. ##### `hashlimit_htable_max` -Maximum entries in the hash. +Data type: `Optional[Integer]` + + Maximum entries in the hash. ##### `hashlimit_htable_size` -The number of buckets of the hash table +Data type: `Optional[Integer]` + + The number of buckets of the hash table ##### `hashlimit_mode` -A comma-separated list of objects to take into consideration. If no --hashlimit-mode option is given, hashlimit acts like limit, but at the expensive of doing the hash housekeeping. -Allowed values are: srcip, srcport, dstip, dstport +Data type: `Optional[Pattern[/^(?:srcip|srcport|dstip|dstport)(?:\,(?:srcip|srcport|dstip|dstport))*$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + A comma-separated list of objects to take into consideration. + If no --hashlimit-mode option is given, hashlimit acts like limit, but at the expensive of doing the hash housekeeping. + Allowed values are: srcip, srcport, dstip, dstport ##### `hashlimit_name` -The name for the /proc/net/ipt_hashlimit/foo entry. -This parameter is required. +Data type: `Optional[String[1]]` + + The name for the /proc/net/ipt_hashlimit/foo entry. + This parameter and either `hashlimit_upto` or `hashlimit_above` are required when setting any other hashlimit values. ##### `hashlimit_srcmask` -When --hashlimit-mode srcip is used, all source addresses encountered will be grouped according to the given prefix length and the so-created subnet will be subject to hashlimit. prefix must be between (inclusive) 0 and 32. Note that --hashlimit-srcmask 0 is basically doing the same thing as not specifying srcip for --hashlimit-mode, but is technically more expensive. +Data type: `Optional[Integer[0,32]]` + + When --hashlimit-mode srcip is used, all source addresses encountered will be grouped according to the given prefix length + and the so-created subnet will be subject to hashlimit. + Prefix must be between (inclusive) 0 and 32. + Note that --hashlimit-srcmask 0 is basically doing the same thing as not specifying srcip for --hashlimit-mode, but is technically more expensive. ##### `hashlimit_upto` -Match if the rate is below or equal to amount/quantum. It is specified either as a number, with an optional time quantum suffix (the default is 3/hour), or as amountb/second (number of bytes per second). -This parameter or hashlimit_above is required. -Allowed forms are '40','40/second','40/minute','40/hour','40/day'. +Data type: `Optional[Pattern[/^\d+(?:\/(?:sec|min|hour|day))?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Match if the rate is below or equal to amount/quantum. It is specified either as a number, with an optional time quantum suffix (the default is 3/hour), or as amountb/second (number of bytes per second). + This parameter or `hashlimit_above` and `hashlimit_name` are required when setting any other hashlimit values. + Allowed forms are '40','40/sec','40/min','40/hour','40/day'. ##### `helper` -Invoke the nf_conntrack_xxx helper module for this packet. +Data type: `Optional[String[1]]` + + Invoke the nf_conntrack_xxx helper module for this packet. ##### `hop_limit` -Valid values: `%r{^\d+$}` +Data type: `Optional[Variant[Pattern[/^(?:!\s)?\d+$/],Integer]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -Hop limiting value for matched packets. + Hop limiting value for matched packets. + To negate add a space seperated `!` the the beginning of the value + This is IPv6 specific. ##### `icmp` -When matching ICMP packets, this is the type of ICMP packet to match. +Data type: `Optional[Variant[String[1],Integer]]` + + When matching ICMP packets, this is the type of ICMP packet to match. -A value of "any" is not supported. To achieve this behaviour the -parameter should simply be omitted or undefined. -An array of values is also not supported. To match against multiple ICMP -types, please use separate rules for each ICMP type. + A value of "any" is not supported. To achieve this behaviour the + parameter should simply be omitted or undefined. + An array of values is also not supported. To match against multiple ICMP + types, please use separate rules for each ICMP type. ##### `iniface` -Valid values: `%r{^!?\s?[a-zA-Z0-9\-\._\+\:@]+$}` +Data type: `Optional[Pattern[/^(?:!\s)?[a-zA-Z0-9\-\._\+\:@]+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -Input interface to filter on. Supports interface alias like eth0:0. -To negate the match try this: + Input interface to filter on. Supports interface alias like eth0:0. + To negate the match try this: - iniface => '! lo', + iniface => '! lo', ##### `ipsec_dir` -Valid values: `in`, `out` +Data type: `Optional[Enum['in', 'out']]` -Sets the ipsec policy direction + Sets the ipsec policy direction ##### `ipsec_policy` -Valid values: `none`, `ipsec` +Data type: `Optional[Enum['none', 'ipsec']]` -Sets the ipsec policy type. May take a combination of arguments for any flags that can be passed to `--pol ipsec` such as: `--strict`, `--reqid 100`, `--next`, `--proto esp`, etc. + Sets the ipsec policy type. May take a combination of arguments for any flags that can be passed to `--pol ipsec` such as: `--strict`, `--reqid 100`, `--next`, `--proto esp`, etc. ##### `ipset` -Matches against the specified ipset list. -Requires ipset kernel module. Will accept a single element or an array. -The value is the name of the blacklist, followed by a space, and then -'src' and/or 'dst' separated by a comma. -For example: 'blacklist src,dst' +Data type: `Optional[Variant[Pattern[/^(?:!\s)?\w+\s(?:src|dst)(?:,src|,dst)?$/], Array[Pattern[/^(?:!\s)?\w+\s(?:src|dst)(?:,src|,dst)?$/]]]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Matches against the specified ipset list. + Requires ipset kernel module. Will accept a single element or an array. + The value is the name of the denylist, followed by a space, and then + 'src' and/or 'dst' separated by a comma. + For example: 'denylist src,dst' + To negate simply place a space seperated `!` at the beginning of a value. + Values can de negated independently. ##### `ipvs` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Indicates that the current packet belongs to an IPVS connection. + Match using Linux Socket Filter. Expects a BPF program in decimal format. + This is the format generated by the nfbpf_compile utility. ##### `isfirstfrag` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -If true, matches if the packet is the first fragment. -Sadly cannot be negated. ipv6. + Matches if the packet is the first fragment. + Specific to IPv6. ##### `isfragment` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Set to true to match tcp fragments (requires type to be set to tcp) + Set to true to match tcp fragments (requires proto to be set to tcp) ##### `ishasmorefrags` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -If true, matches if the packet has it's 'more fragments' bit set. ipv6. + Matches if the packet has it's 'more fragments' bit set. + Specific to IPv6. ##### `islastfrag` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -If true, matches if the packet is the last fragment. ipv6. + Matches if the packet is the last fragment. + Specific to IPv6. ##### `jump` -The value for the iptables --jump parameter. Normal values are: +Data type: `Optional[Pattern[/^[a-zA-Z0-9_]+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -* QUEUE -* RETURN -* DNAT -* SNAT -* LOG -* NFLOG -* MASQUERADE -* REDIRECT -* MARK -* CT + This value for the iptables --jump parameter and the action to perform on a match. Common values are: -But any valid chain name is allowed. + * ACCEPT - the packet is accepted + * REJECT - the packet is rejected with a suitable ICMP response + * DROP - the packet is dropped -For the values ACCEPT, DROP, and REJECT, you must use the generic -'action' parameter. This is to enfore the use of generic parameters where -possible for maximum cross-platform modelling. + But can also be on of the following: -If you set both 'accept' and 'jump' parameters, you will get an error as -only one of the options should be set. + * QUEUE + * RETURN + * DNAT + * SNAT + * LOG + * NFLOG + * NETMAP + * MASQUERADE + * REDIRECT + * MARK + * CT + + And any valid chain name is also allowed. + + If you specify no value it will simply match the rule but perform no action. ##### `kernel_timezone` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Use the kernel timezone instead of UTC to determine whether a packet meets the time regulations. + Use the kernel timezone instead of UTC to determine whether a packet meets the time regulations. ##### `length` -Sets the length of layer-3 payload to match. +Data type: `Optional[Pattern[/^([0-9]+)(:)?([0-9]+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Sets the length of layer-3 payload to match. + + Example values are: '500', '5:400' ##### `limit` -Rate limiting value for matched packets. The format is: -rate/[/second/|/minute|/hour|/day]. +Data type: `Optional[Pattern[/^\d+\/(?:sec(?:ond)?|min(?:ute)?|hour|day)$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Rate limiting value for matched packets. The format is: + rate/[/second/|/minute|/hour|/day] + + Example values are: '50/sec', '40/min', '30/hour', '10/day'." -Example values are: '50/sec', '40/min', '30/hour', '10/day'." +##### `line` + +Data type: `Optional[String[1]]` + + A read only attribute containing the full rule, used when deleting and when applying firewallchain purge attributes. ##### `log_ip_options` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -When combined with jump => "LOG" logging of the TCP IP/IPv6 -packet header. + When combined with jump => "LOG" logging of the TCP IP/IPv6 packet header. ##### `log_level` -When combined with jump => "LOG" specifies the system log level to log -to. +Data type: `Optional[Variant[Integer[0,7],String[1]]]` + + When combined with jump => "LOG" specifies the system log level to log to. + + Note: log level 4/warn is the default setting and as such it is not returned by iptables-save. + As a result, explicitly setting `log_level` to this can result in idempotency errors. ##### `log_prefix` -When combined with jump => "LOG" specifies the log prefix to use when -logging. +Data type: `Optional[String[1]]` + + When combined with jump => "LOG" specifies the log prefix to use when logging. ##### `log_tcp_options` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -When combined with jump => "LOG" logging of the TCP packet -header. + When combined with jump => "LOG" logging of the TCP packet header. ##### `log_tcp_sequence` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -When combined with jump => "LOG" enables logging of the TCP sequence -numbers. + When combined with jump => "LOG" enables logging of the TCP sequence numbers. ##### `log_uid` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -When combined with jump => "LOG" specifies the uid of the process making -the connection. + When combined with jump => "LOG" specifies the uid of the process making the connection. ##### `mac_source` -Valid values: `%r{^([0-9a-f]{2}[:]){5}([0-9a-f]{2})$}i` +Data type: `Optional[Pattern[/^(?:!\s)?([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -MAC Source + MAC Source ##### `mask` -Sets the mask to use when `recent` is enabled. +Data type: `Optional[Pattern[/^\d+\.\d+\.\d+\.\d+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Recent module; sets the mask to use when `recent` is enabled. + The recent module defaults this to `255.255.255.255` when recent is set ##### `match_mark` -Match the Netfilter mark value associated with the packet. Accepts either of: -mark/mask or mark. These will be converted to hex if they are not already. +Data type: `Optional[Pattern[/^(?:!\s)?[a-fA-F0-9x]+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Match the Netfilter mark value associated with the packet, accepts a mark. + This value will be converted to hex if it is not already. + This value can be negated by adding a space seperated `!` to the beginning. ##### `month_days` -Only match on the given days of the month. Possible values are 1 to 31. -Note that specifying 31 will of course not match on months which do not have a 31st day; -the same goes for 28- or 29-day February. +Data type: `Optional[Variant[Integer[0,31], Array[Integer[0,31]]]]` + + Only match on the given days of the month. Possible values are 1 to 31. + Note that specifying 31 will of course not match on months which do not have a 31st day; + the same goes for 28-day or 29-day February. + + Can be passed either as a single value or an array of values: + month_days => 5, + month_days => [5, 9, 23], ##### `mss` -Match a given TCP MSS value or range. +Data type: `Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Match a given TCP MSS value or range. + This value can be negated by adding a space seperated `!` to the beginning. ##### `nflog_group` -Used with the jump target NFLOG. -The netlink group (0 - 2^16-1) to which packets are (only applicable -for nfnetlink_log). Defaults to 0. +Data type: `Optional[Integer[1, 65535]]` + + Used with the jump target NFLOG. + The netlink group (0 - 2^16-1) to which packets are (only applicable + for nfnetlink_log). Defaults to 0. ##### `nflog_prefix` -Used with the jump target NFLOG. -A prefix string to include in the log message, up to 64 characters long, -useful for distinguishing messages in the logs. +Data type: `Optional[String]` + + Used with the jump target NFLOG. + A prefix string to include in the log message, up to 64 characters long, + useful for distinguishing messages in the logs. ##### `nflog_range` -Used with the jump target NFLOG. -This has never worked, use nflog_size instead. +Data type: `Optional[Integer[1]]` + + Used with the jump target NFLOG. + This has never worked, use nflog_size instead. ##### `nflog_size` -Used with the jump target NFLOG. -The number of bytes to be copied to userspace (only applicable for nfnetlink_log). -nfnetlink_log instances may specify their own size, this option overrides it. +Data type: `Optional[Integer[1]]` + + Used with the jump target NFLOG. + The number of bytes to be copied to userspace (only applicable for nfnetlink_log). + nfnetlink_log instances may specify their own size, this option overrides it. ##### `nflog_threshold` -Used with the jump target NFLOG. -Number of packets to queue inside the kernel before sending them to userspace -(only applicable for nfnetlink_log). Higher values result in less overhead -per packet, but increase delay until the packets reach userspace. Defaults to 1. +Data type: `Optional[Integer[1]]` + + Used with the jump target NFLOG. + Number of packets to queue inside the kernel before sending them to userspace + (only applicable for nfnetlink_log). Higher values result in less overhead + per packet, but increase delay until the packets reach userspace. Defaults to 1. ##### `notrack` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Invoke the disable connection tracking for this packet. -This parameter can be used with iptables version >= 1.8.3 + Invoke the disable connection tracking for this packet. + This parameter can be used with iptables version >= 1.8.3 ##### `outiface` -Valid values: `%r{^!?\s?[a-zA-Z0-9\-\._\+\:@]+$}` +Data type: `Optional[Pattern[/^(?:!\s)?[a-zA-Z0-9\-\._\+\:@]+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ - Output interface to filter on. Supports interface alias like eth0:0. -To negate the match try this: + Output interface to filter on. Supports interface alias like eth0:0. + To negate the match try this: - outiface => '! lo', + outiface => '! lo', ##### `physdev_in` -Valid values: `%r{^[a-zA-Z0-9\-\._\+]+$}` +Data type: `Optional[Pattern[/^(?:!\s)?[a-zA-Z0-9\-\._\+]+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -Match if the packet is entering a bridge from the given interface. + Match if the packet is entering a bridge from the given interface. + To negate the match try this: + + physdev_in => '! lo', ##### `physdev_is_bridged` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Match if the packet is transversing a bridge. + Match if the packet is transversing a bridge. ##### `physdev_is_in` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Matches if the packet has entered through a bridge interface. + Matches if the packet has entered through a bridge interface. ##### `physdev_is_out` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Matches if the packet will leave through a bridge interface. + Matches if the packet will leave through a bridge interface. ##### `physdev_out` -Valid values: `%r{^[a-zA-Z0-9\-\._\+]+$}` - -Match if the packet is leaving a bridge via the given interface. - -##### `pkttype` +Data type: `Optional[Pattern[/^(?:!\s)?[a-zA-Z0-9\-\._\+]+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -Valid values: `unicast`, `broadcast`, `multicast` + Match if the packet is leaving a bridge via the given interface. + To negate the match try this: -Sets the packet type to match. + physdev_out => '! lo', -##### `port` +##### `pkttype` -*note* This property has been DEPRECATED +Data type: `Optional[Enum['unicast', 'broadcast', 'multicast']]` -The destination or source port to match for this filter (if the protocol -supports ports). Will accept a single element or an array. + Sets the packet type to match. -For some firewall providers you can pass a range of ports in the format: +##### `proto` - - +Data type: `Optional[Pattern[/^(?:!\s)?(?:ip(?:encap)?|tcp|udp|icmp|esp|ah|vrrp|carp|igmp|ipv4|ospf|gre|cbt|sctp|pim|all)/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -For example: + The specific protocol to match for this rule. - 1-1024 +Default value: `tcp` -This would cover ports 1 to 1024. +##### `protocol` -##### `proto` +Data type: `Enum['iptables', 'ip6tables', 'IPv4', 'IPv6']` -Valid values: `[:ip, :tcp, :udp, :icmp, :"ipv6-icmp", :esp, :ah, :vrrp, :carp, :igmp, :ipencap, :ipv4, :ipv6, :ospf, :gre, :cbt, :sctp, :pim, :all].map { |proto| - [proto, "! #{proto}".to_sym] - }.flatten` + The protocol used to set the rule, it's allowed values have been expanded to bring it closer to its `firewallchain` counterpart. + Defaults to `IPv4` -The specific protocol to match for this rule. + Noted: this was previously defined as `provider`, however the resource_api does not allow this to be used as an attribute title. -Default value: `tcp` +Default value: `IPv4` ##### `queue_bypass` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Used with NFQUEUE jump target -Allow packets to bypass :queue_num if userspace process is not listening + Allow packets to bypass :queue_num if userspace process is not listening ##### `queue_num` -Used with NFQUEUE jump target. -What queue number to send packets to +Data type: `Optional[Integer[1]]` + + Used with NFQUEUE jump target. + What queue number to send packets to ##### `random` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" -this boolean will enable randomized port mapping. + When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" this boolean will enable randomized port mapping. ##### `random_fully` -Valid values: `true`, `false` - -When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" -this boolean will enable fully randomized port mapping. +Data type: `Optional[Boolean]` -**NOTE** Requires Kernel >= 3.13 and iptables >= 1.6.2 + When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" this boolean will enable fully randomized port mapping. ##### `rdest` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Recent module; add the destination IP address to the list. -Must be boolean true. + Recent module; add the destination IP address to the list. + Mutually exclusive with `rsource` + Must be boolean true. ##### `reap` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Recent module; can only be used in conjunction with the `rseconds` -attribute. When used, this will cause entries older than 'seconds' to be -purged. Must be boolean true. + Recent module; can only be used in conjunction with the `rseconds` + attribute. When used, this will cause entries older than 'seconds' to be + purged. Must be boolean true. ##### `recent` -Valid values: `set`, `update`, `rcheck`, `remove` - -Enable the recent module. Takes as an argument one of set, update, -rcheck or remove. For example: - - ``` - # If anyone's appeared on the 'badguy' blacklist within - # the last 60 seconds, drop their traffic, and update the timestamp. - firewall { '100 Drop badguy traffic': - recent => 'update', - rseconds => 60, - rsource => true, - rname => 'badguy', - action => 'DROP', - chain => 'FORWARD', - } - ``` - - - ``` - # No-one should be sending us traffic on eth0 from the - # localhost, Blacklist them - firewall { '101 blacklist strange traffic': - recent => 'set', - rsource => true, - rname => 'badguy', - destination => '127.0.0.0/8', - iniface => 'eth0', - action => 'DROP', - chain => 'FORWARD', - } - ``` +Data type: `Optional[Enum['set', 'update', 'rcheck', 'remove', '! set', '! update', '! rcheck', '! remove']]` + + Enable the recent module. Takes as an argument one of set, update, + rcheck or remove. For example: + + ``` + # If anyone's appeared on the 'badguy' blacklist within + # the last 60 seconds, drop their traffic, and update the timestamp. + firewall { '100 Drop badguy traffic': + recent => 'update', + rseconds => 60, + rsource => true, + rname => 'badguy', + jump => 'DROP', + chain => 'FORWARD', + } + ``` + + + ``` + # No-one should be sending us traffic on eth0 from the + # localhost, Blacklist them + firewall { '101 blacklist strange traffic': + recent => 'set', + rsource => true, + rname => 'badguy', + destination => '127.0.0.0/8', + iniface => 'eth0', + jump => 'DROP', + chain => 'FORWARD', + } + ``` ##### `reject` -When combined with action => "REJECT" you can specify a different icmp -response to be sent back to the packet sender. +Data type: `Optional[Enum['icmp-net-unreachable', 'icmp-host-unreachable', 'icmp-port-unreachable', 'icmp-proto-unreachable', + 'icmp-net-prohibited', 'icmp-host-prohibited', 'icmp-admin-prohibited', 'icmp6-no-route', 'no-route', + 'icmp6-adm-prohibited', 'adm-prohibited', 'icmp6-addr-unreachable', 'addr-unreach', 'icmp6-port-unreachable']]` + + When combined with jump => "REJECT" you can specify a different icmp response to be sent back to the packet sender. + Valid values differ depending on if the protocol is `IPv4` or `IPv6`. + IPv4 allows: icmp-net-unreachable, icmp-host-unreachable, icmp-port-unreachable, icmp-proto-unreachable, icmp-net-prohibited, + icmp-host-prohibited, or icmp-admin-prohibited. + IPv6 allows: icmp6-no-route, no-route, icmp6-adm-prohibited, adm-prohibited, icmp6-addr-unreachable, addr-unreach, or icmp6-port-unreachable. ##### `rhitcount` -Recent module; used in conjunction with `recent => 'update'` or `recent -=> 'rcheck'. When used, this will narrow the match to only happen when -the address is in the list and packets had been received greater than or -equal to the given value. +Data type: `Optional[Integer[1]]` + + Recent module; used in conjunction with `recent => 'update'` or `recent + => 'rcheck'. When used, this will narrow the match to only happen when + the address is in the list and packets had been received greater than or + equal to the given value. ##### `rname` -Recent module; The name of the list. Takes a string argument. +Data type: `Optional[String[1]]` + + Recent module; The name of the list. + The recent module defaults this to `DEFAULT` when recent is set ##### `rpfilter` -Valid values: `loose`, `validmark`, `accept-local`, `invert` +Data type: `Optional[Variant[Enum['loose', 'validmark', 'accept-local', 'invert'], Array[Enum['loose', 'validmark', 'accept-local', 'invert']]]]` -Enable the rpfilter module. + Enable the rpfilter module. ##### `rseconds` -Recent module; used in conjunction with one of `recent => 'rcheck'` or -`recent => 'update'`. When used, this will narrow the match to only -happen when the address is in the list and was seen within the last given -number of seconds. +Data type: `Optional[Integer[1]]` + + Recent module; used in conjunction with one of `recent => 'rcheck'` or + `recent => 'update'`. When used, this will narrow the match to only + happen when the address is in the list and was seen within the last given + number of seconds. ##### `rsource` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Recent module; add the source IP address to the list. -Must be boolean true. + Recent module; add the source IP address to the list. + Mutually exclusive with `rdest` + The recent module defaults this behaviour to true when recent is set. ##### `rttl` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -Recent module; may only be used in conjunction with one of `recent => -'rcheck'` or `recent => 'update'`. When used, this will narrow the match -to only happen when the address is in the list and the TTL of the current -packet matches that of the packet which hit the `recent => 'set'` rule. -This may be useful if you have problems with people faking their source -address in order to DoS you via this module by disallowing others access -to your site by sending bogus packets to you. Must be boolean true. + Recent module; may only be used in conjunction with one of `recent => + 'rcheck'` or `recent => 'update'`. When used, this will narrow the match + to only happen when the address is in the list and the TTL of the current + packet matches that of the packet which hit the `recent => 'set'` rule. + This may be useful if you have problems with people faking their source + address in order to DoS you via this module by disallowing others access + to your site by sending bogus packets to you. Must be boolean true. ##### `set_dscp` -Set DSCP Markings. +Data type: `Optional[String[1]]` + + Set DSCP Markings. ##### `set_dscp_class` -This sets the DSCP field according to a predefined DiffServ class. +Data type: `Optional[Enum['af11', 'af12', 'af13', 'af21', 'af22', 'af23', 'af31', 'af32', 'af33', 'af41', 'af42', 'af43', 'cs1', 'cs2', 'cs3', 'cs4', 'cs5', 'cs6', 'cs7', 'ef']]` + + This sets the DSCP field according to a predefined DiffServ class. ##### `set_mark` -Set the Netfilter mark value associated with the packet. Accepts either of: -mark/mask or mark. These will be converted to hex if they are not already. +Data type: `Optional[Pattern[/^[a-fA-F0-9x]+(?:\/[a-fA-F0-9x]+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Set the Netfilter mark value associated with the packet. Accepts either of mark/mask or mark. + These will be converted to hex if they are not already. ##### `set_mss` -Sets the TCP MSS value for packets. +Data type: `Optional[Integer[1]]` + + Sets the TCP MSS value for packets. ##### `socket` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -If true, matches if an open socket can be found by doing a coket lookup -on the packet. + If true, matches if an open socket can be found by doing a coket lookup + on the packet. ##### `source` -The source address. For example: +Data type: `Optional[String[1]]` + + The source address. For example: - source => '192.168.2.0/24' + source => '192.168.2.0/24' -You can also negate a mask by putting ! in front. For example: + You can also negate a mask by putting ! in front. For example: - source => '! 192.168.2.0/24' + source => '! 192.168.2.0/24' -The source can also be an IPv6 address if your provider supports it. + The source can also be an IPv6 address if your provider supports it. ##### `sport` -The source port to match for this filter (if the protocol supports -ports). Will accept a single element or an array. +Data type: `Optional[Variant[Array[Variant[Pattern[/^(?:!\s)?\d+(?:(?:\:|-)\d+)?$/],Integer]],Pattern[/^(?:!\s)?\d+(?:(?:\:|-)\d+)?$/],Integer]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + The source port to match for this filter (if the protocol supports + ports). Will accept a single element or an array. -For some firewall providers you can pass a range of ports in the format: + For some firewall providers you can pass a range of ports in the format: - - + sport => '1:1024' -For example: + This would cover ports 1 to 1024. - 1-1024 + You can also negate a port by putting ! in front. For example: -This would cover ports 1 to 1024. + sport => '! 54' + + If you wish to negate multiple ports at once, then place a ! at the start of the first array + variable. For example: + + sport => ['! 54','23'] + + Note: + This will negate all passed ports, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. ##### `src_cc` -Valid values: `%r{^[A-Z]{2}(,[A-Z]{2})*$}` +Data type: `Optional[Pattern[/^[A-Z]{2}(,[A-Z]{2})*$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -src attribute for the module geoip + src attribute for the module geoip ##### `src_range` -The source IP range. For example: +Data type: `Optional[String[1]]` + + The source IP range. For example: + + src_range => '192.168.1.1-192.168.1.10' - src_range => '192.168.1.1-192.168.1.10' + You can also negate the range by apending a `!`` to the front. For example: -The source IP range must be in 'IP1-IP2' format. + src_range => '! 192.168.1.1-192.168.1.10' + + The source IP range must be in 'IP1-IP2' format. ##### `src_type` -Valid values: `[:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, - :BLACKHOLE, :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].map { |address_type| - [ - address_type, - "! #{address_type}".to_sym, - "#{address_type} --limit-iface-in".to_sym, - "#{address_type} --limit-iface-out".to_sym, - "! #{address_type} --limit-iface-in".to_sym, - "! #{address_type} --limit-iface-out".to_sym, - ] - }.flatten` +Data type: `Optional[Variant[ + Array[Pattern[/^(?:!\s)?(?:UNSPEC|UNICAST|LOCAL|BROADCAST|ANYCAST|MULTICAST|BLACKHOLE|UNREACHABLE|UNREACHABLE|PROHIBIT|THROW|NAT|XRESOLVE)(?:\s--limit-iface-(?:in|out))?$/]], + Pattern[/^(?:!\s)?(?:UNSPEC|UNICAST|LOCAL|BROADCAST|ANYCAST|MULTICAST|BLACKHOLE|UNREACHABLE|UNREACHABLE|PROHIBIT|THROW|NAT|XRESOLVE)(?:\s--limit-iface-(?:in|out))?$/]]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -The source address type. For example: + The source address type. For example: - src_type => ['LOCAL'] + src_type => 'LOCAL' -Can be one of: + Can be one of: -* UNSPEC - an unspecified address -* UNICAST - a unicast address -* LOCAL - a local address -* BROADCAST - a broadcast address -* ANYCAST - an anycast packet -* MULTICAST - a multicast address -* BLACKHOLE - a blackhole address -* UNREACHABLE - an unreachable address -* PROHIBIT - a prohibited address -* THROW - undocumented -* NAT - undocumented -* XRESOLVE - undocumented + * UNSPEC - an unspecified address + * UNICAST - a unicast address + * LOCAL - a local address + * BROADCAST - a broadcast address + * ANYCAST - an anycast packet + * MULTICAST - a multicast address + * BLACKHOLE - a blackhole address + * UNREACHABLE - an unreachable address + * PROHIBIT - a prohibited address + * THROW - undocumented + * NAT - undocumented + * XRESOLVE - undocumented -In addition, it accepts '--limit-iface-in' and '--limit-iface-out' flags, specified as: + In addition, it accepts '--limit-iface-in' and '--limit-iface-out' flags, specified as: - src_type => ['LOCAL --limit-iface-in'] + src_type => ['LOCAL --limit-iface-in'] -It can also be negated using '!': + It can also be negated using '!': - src_type => ['! LOCAL'] + src_type => ['! LOCAL'] -Will accept a single element or an array. + Will accept a single element or an array. Each element of the array should be negated seperately. ##### `stat_every` -Match one packet every nth packet. Requires `stat_mode => 'nth'` +Data type: `Optional[Integer[1]]` + + Match one packet every nth packet. Requires `stat_mode => 'nth'` ##### `stat_mode` -Valid values: `nth`, `random` +Data type: `Optional[Enum[nth, random]]` -Set the matching mode for statistic matching. + Set the matching mode for statistic matching. ##### `stat_packet` -Valid values: `%r{^\d+$}` +Data type: `Optional[Integer]` -Set the initial counter value for the nth mode. Must be between 0 and the value of `stat_every`. Defaults to 0. Requires `stat_mode => 'nth'` + Set the initial counter value for the nth mode. Must be between 0 and the value of `stat_every`. + Defaults to 0. Requires `stat_mode => 'nth'` ##### `stat_probability` -Set the probability from 0 to 1 for a packet to be randomly matched. It works only with `stat_mode => 'random'`. +Data type: `Optional[Variant[Integer[0,1], Float[0.0,1.0]]]` + + Set the probability from 0 to 1 for a packet to be randomly matched. It works only with `stat_mode => 'random'`. ##### `state` -Valid values: `INVALID`, `ESTABLISHED`, `NEW`, `RELATED`, `UNTRACKED` +Data type: `Optional[Variant[Pattern[/^(?:!\s)?(?:INVALID|ESTABLISHED|NEW|RELATED|UNTRACKED)$/], Array[Pattern[/^(?:!\s)?(?:INVALID|ESTABLISHED|NEW|RELATED|UNTRACKED)$/]]]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Matches a packet based on its state in the firewall stateful inspection + table. Values can be: + + * INVALID + * ESTABLISHED + * NEW + * RELATED + * UNTRACKED + * SNAT + * DNAT + + Can be passed either as a single String or as an Array: -Matches a packet based on its state in the firewall stateful inspection -table. Values can be: + state => 'INVALID' + state => ['INVALID', 'ESTABLISHED'] -* INVALID -* ESTABLISHED -* NEW -* RELATED -* UNTRACKED + Values can be negated by adding a '!'. + If you wish to negate multiple states at once, then place a ! at the start of the first array + variable. For example: + + state => ['! INVALID', 'ESTABLISHED'] + + Note: + This will negate all passed states, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. ##### `string` -String matching feature. Matches the packet against the pattern -given as an argument. +Data type: `Optional[String[1]]` + + String matching feature. Matches the packet against the pattern + given as an argument. + To negate, add a space seperated `!` to the beginning of the string. ##### `string_algo` -Valid values: `bm`, `kmp` +Data type: `Optional[Enum['bm', 'kmp']]` -String matching feature, pattern matching strategy. + String matching feature, pattern matching strategy. ##### `string_from` -String matching feature, offset from which we start looking for any matching. +Data type: `Optional[Integer[1]]` + + String matching feature, offset from which we start looking for any matching. ##### `string_hex` -String matching feature. Matches the package against the hex pattern -given as an argument. +Data type: `Optional[Pattern[/^(?:!\s)?\|[a-zA-Z0-9\s]+\|$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + String matching feature. Matches the packet against the pattern + given as an argument. + To negate, add a space seperated `!` to the beginning of the string. ##### `string_to` -String matching feature, offset up to which we should scan. +Data type: `Optional[Integer[1]]` + + String matching feature, offset up to which we should scan. ##### `table` -Valid values: `nat`, `mangle`, `filter`, `raw`, `rawpost` +Data type: `Enum['nat', 'mangle', 'filter', 'raw', 'rawpost', 'broute', 'security']` + + The table the rule will exist in. + Valid options are: -Table to use. Can be one of: + * nat + * mangle + * filter + * raw + * rawpost -* nat -* mangle -* filter -* raw -* rawpost + Defaults to 'filter' Default value: `filter` ##### `tcp_flags` -Match when the TCP flags are as specified. -Is a string with a list of comma-separated flag names for the mask, -then a space, then a comma-separated list of flags that should be set. -The flags are: SYN ACK FIN RST URG PSH ALL NONE -Note that you specify them in the order that iptables --list-rules -would list them to avoid having puppet think you changed the flags. -Example: FIN,SYN,RST,ACK SYN matches packets with the SYN bit set and the -ACK,RST and FIN bits cleared. Such packets are used to request -TCP connection initiation. +Data type: `Optional[Pattern[/^(?:!\s)?((FIN|SYN|RST|PSH|ACK|URG|ALL|NONE),?)+\s((FIN|SYN|RST|PSH|ACK|URG|ALL|NONE),?)+$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Match when the TCP flags are as specified. + Is a string with a list of comma-separated flag names for the mask, + then a space, then a comma-separated list of flags that should be set. + The flags are: FIN SYN RST PSH ACK URG ALL NONE + Note that you specify them in the order that iptables --list-rules + would list them to avoid having puppet think you changed the flags. + + Example: FIN,SYN,RST,ACK SYN matches packets with the SYN bit set and the + ACK,RST and FIN bits cleared. Such packets are used to request + TCP connection initiation. + Can be negated by placing ! in front, i.e. + ! FIN,SYN,RST,ACK SYN + +##### `tcp_option` + +Data type: `Optional[Variant[Pattern[/^(?:!\s)?(?:[0-1][0-9]{0,2}|2[0-4][0-9]|25[0-5])$/], Integer[0,255]]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Match when the TCP option is present or absent. + Given as a single TCP option, optionally prefixed with '! ' to match + on absence instead. Only one TCP option can be matched in a given rule. + TCP option numbers are an eight-bit field, so valid option numbers range + from 0-255. ##### `time_contiguous` -Valid values: `true`, `false` +Data type: `Optional[Boolean]` -When time_stop is smaller than time_start value, match this as a single time period instead distinct intervals. + When time_stop is smaller than time_start value, match this as a single time period instead distinct intervals. ##### `time_start` -Only match during the given daytime. The possible time range is 00:00:00 to 23:59:59. -Leading zeroes are allowed (e.g. "06:03") and correctly interpreted as base-10. +Data type: `Optional[Pattern[/^([0-9]|[0-1][0-9]|2[0-3])\:[0-5][0-9](?:\:[0-5][0-9])?/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Only match during the given daytime. The possible time range is 00:00:00 to 23:59:59. + Leading zeroes are allowed (e.g. "06:03") and correctly interpreted as base-10. ##### `time_stop` -Only match during the given daytime. The possible time range is 00:00:00 to 23:59:59. -Leading zeroes are allowed (e.g. "06:03") and correctly interpreted as base-10. +Data type: `Optional[Pattern[/^([0-9]|[0-1][0-9]|2[0-3])\:[0-5][0-9](?:\:[0-5][0-9])?/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Only match during the given daytime. The possible time range is 00:00:00 to 23:59:59. + Leading zeroes are allowed (e.g. "06:03") and correctly interpreted as base-10. ##### `to` -For NETMAP this will replace the destination IP +Data type: `Optional[String[1]]` + + For NETMAP this will replace the destination IP ##### `todest` -When using jump => "DNAT" you can specify the new destination address -using this paramter. +Data type: `Optional[String[1]]` + + When using jump => "DNAT" you can specify the new destination address using this paramter. + Can specify a single new destination IP address or an inclusive range of IP addresses. + Optionally a port or a port range with a possible follow up baseport can be provided. + Input structure: [ipaddr[-ipaddr]][:port[-port[/baseport]]] ##### `toports` -For DNAT this is the port that will replace the destination port. +Data type: `Optional[Pattern[/^\d+(?:-\d+)?$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + For REDIRECT/MASQUERADE this is the port that will replace the destination/source port. + Can specify a single new port or an inclusive range of ports. ##### `tosource` -When using jump => "SNAT" you can specify the new source address using -this parameter. +Data type: `Optional[String[1]]` + + When using jump => "SNAT" you can specify the new source address using this paramter. + Can specify a single new destination IP address or an inclusive range of IP addresses. + Input structure: [ipaddr[-ipaddr]][:port[-port]] + +##### `u32` + +Data type: `Optional[Pattern[/^0x[0-9a-fA-F]+&0x[0-9a-fA-F]+=0x[0-9a-fA-F]+(?::0x[0-9a-fA-F]+)?(?:&&0x[0-9a-fA-F]+&0x[0-9a-fA-F]+=0x[0-9a-fA-F]+(?::0x[0-9a-fA-F]+)?)*$/]]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ + + Enable the u32 module. Takes as an argument one of set, update, + rcheck or remove. For example: + firewall { '032 u32 test': + ensure => present, + table => 'mangle', + chain => 'PREROUTING', + u32 => '0x4&0x1fff=0x0&&0x0&0xf000000=0x5000000', + jump => 'DROP', + } ##### `uid` -UID or Username owner matching rule. Accepts a string argument -only, as iptables does not accept multiple uid in a single -statement. +Data type: `Optional[Variant[String[1], Integer]]` + + UID or Username owner matching rule. Accepts a single argument + only, as iptables does not accept multiple uid in a single + statement. + To negate add a space seperated '!' in front of the value. ##### `week_days` -Valid values: `Mon`, `Tue`, `Wed`, `Thu`, `Fri`, `Sat`, `Sun` +Data type: `Optional[Variant[Enum['Mon','Tue','Wed','Thu','Fri','Sat','Sun'], Array[Enum['Mon','Tue','Wed','Thu','Fri','Sat','Sun']]]]` -Only match on the given weekdays. + Only match on the given weekdays. + + Can be passed either as a single value or an array of values: + week_days => 'Mon', + week_days => ['Mon', 'Tue', 'Wed'], ##### `zone` -Assign this packet to zone id and only have lookups done in that zone. +Data type: `Optional[Integer]` + + Assign this packet to zone id and only have lookups done in that zone. #### Parameters The following parameters are available in the `firewall` type. -* [`line`](#-firewall--line) * [`name`](#-firewall--name) -* [`provider`](#-firewall--provider) - -##### `line` - -Read-only property for caching the rule line. ##### `name` -Valid values: `%r{^\d+[[:graph:][:space:]]+$}` - namevar -The canonical name of the rule. This name is also used for ordering -so make sure you prefix the rule with a number: - - 000 this runs first - 999 this runs last +Data type: `Pattern[/(^\d+(?:[ \t-]\S+)+$)/]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -Depending on the provider, the name of the rule can be stored using -the comment feature of the underlying firewall subsystem. + The canonical name of the rule. This name is also used for ordering + so make sure you prefix the rule with a number: -##### `provider` + 000 this runs first + 999 this runs last -The specific backend to use for this `firewall` resource. You will seldom need to specify this --- Puppet will usually -discover the appropriate provider for your platform. + Depending on the provider, the name of the rule can be stored using + the comment feature of the underlying firewall subsystem. ### `firewallchain` +This type provides the capability to manage rule chains for firewalls. + Currently this supports only iptables, ip6tables and ebtables on Linux. And provides support for setting the default policy on chains and tables that allow it. -**Autorequires:** -If Puppet is managing the iptables, iptables-persistent, or iptables-services packages, -and the provider is iptables_chain, the firewall resource will autorequire -those packages to ensure that any required binaries are installed. - #### Providers * iptables_chain is the only provider that supports firewallchain. @@ -1432,89 +1732,78 @@ The following properties are available in the `firewallchain` type. ##### `ensure` -Valid values: `present`, `absent` +Data type: `Enum[present, absent]` -The basic property that the resource should be in. + Whether this chain should be present or absent on the target system. + Setting this to absent will first remove all rules associated with this chain and then delete the chain itself. + Inbuilt chains however will merely remove any added rules and, if it has been changed, return their policy to the default. Default value: `present` -##### `policy` +##### `ignore` -Valid values: `accept`, `drop`, `queue`, `return` +Data type: `Optional[Variant[String[1], Array[String[1]]]]` -This is the action to when the end of the chain is reached. -It can only be set on inbuilt chains (INPUT, FORWARD, OUTPUT, -PREROUTING, POSTROUTING) and can be one of: + Regex to perform on firewall rules to exempt unmanaged rules from purging. + This is matched against the output of `iptables-save`. + + This can be a single regex, or an array of them. + To support flags, use the ruby inline flag mechanism. + Meaning a regex such as + /foo/i + can be written as + '(?i)foo' or '(?i:foo)' + + Full example: + ``` + firewallchain { 'INPUT:filter:IPv4': + purge => true, + ignore => [ + '-j fail2ban-ssh', # ignore the fail2ban jump rule + '--comment "[^"]*(?i:ignore)[^"]*"', # ignore any rules with "ignore" (case insensitive) in the comment in the rule + ], + } + ``` + +##### `ignore_foreign` -* accept - the packet is accepted -* drop - the packet is dropped -* queue - the packet is passed userspace -* return - the packet is returned to calling (jump) queue - or the default of inbuilt chains +Data type: `Boolean` -#### Parameters + Ignore rules that do not match the puppet title pattern "^\d+[[:graph:][:space:]]" when purging unmanaged firewall rules in this chain. + This can be used to ignore rules that were not put in by puppet. Beware that nothing keeps other systems from configuring firewall rules with a comment that starts with digits, and is indistinguishable from puppet-configured rules. -The following parameters are available in the `firewallchain` type. +##### `policy` -* [`ignore`](#-firewallchain--ignore) -* [`ignore_foreign`](#-firewallchain--ignore_foreign) -* [`name`](#-firewallchain--name) -* [`provider`](#-firewallchain--provider) -* [`purge`](#-firewallchain--purge) +Data type: `Optional[Enum['accept', 'drop', 'queue', 'return']]` -##### `ignore` + This action to take when the end of the chain is reached. + This can only be set on inbuilt chains (i.e. INPUT, FORWARD, OUTPUT, + PREROUTING, POSTROUTING) and can be one of: -Regex to perform on firewall rules to exempt unmanaged rules from purging (when enabled). -This is matched against the output of `iptables-save`. + * accept - the packet is accepted + * drop - the packet is dropped + * queue - the packet is passed userspace + * return - the packet is returned to calling (jump) queue + or the default of inbuilt chains -This can be a single regex, or an array of them. -To support flags, use the ruby inline flag mechanism. -Meaning a regex such as - /foo/i -can be written as - '(?i)foo' or '(?i:foo)' +##### `purge` -Full example: -``` -firewallchain { 'INPUT:filter:IPv4': - purge => true, - ignore => [ - '-j fail2ban-ssh', # ignore the fail2ban jump rule - '--comment "[^"]*(?i:ignore)[^"]*"', # ignore any rules with "ignore" (case insensitive) in the comment in the rule - ], -} -``` +Data type: `Boolean` -##### `ignore_foreign` +Whether or not to purge unmanaged rules in this chain -Valid values: `false`, `true` +#### Parameters -Ignore rules that do not match the puppet title pattern "^\d+[[:graph:][:space:]]" when purging unmanaged firewall rules -in this chain. -This can be used to ignore rules that were not put in by puppet. Beware that nothing keeps other systems from -configuring firewall rules with a comment that starts with digits, and is indistinguishable from puppet-configured -rules. +The following parameters are available in the `firewallchain` type. -Default value: `false` +* [`name`](#-firewallchain--name) ##### `name` namevar -The canonical name of the chain. - -For iptables the format must be {chain}:{table}:{protocol}. - -##### `provider` +Data type: `Pattern[/^(?:\S+):(?:nat|mangle|filter|raw|rawpost|broute|security):(?:IP(?:v[46])?|ethernet)$/]` +_*this data type contains a regex that may not be accurately reflected in generated documentation_ -The specific backend to use for this `firewallchain` resource. You will seldom need to specify this --- Puppet will -usually discover the appropriate provider for your platform. - -##### `purge` - -Valid values: `false`, `true` - -Purge unmanaged firewall rules in this chain - -Default value: `false` +The canonical name of the chain with the required format being `{chain}:{table}:{protocol}`. diff --git a/lib/puppet/provider/firewall.rb b/lib/puppet/provider/firewall.rb deleted file mode 100644 index f8403a80b..000000000 --- a/lib/puppet/provider/firewall.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -# -# firewall.rb -# -class Puppet::Provider::Firewall < Puppet::Provider - # Prefetch our rule list. This is ran once every time before any other - # action (besides initialization of each object). - def self.prefetch(resources) - debug('[prefetch(resources)]') - instances.each do |prov| - resource = resources[prov.name] || resources[prov.name.downcase] - if resource - resource.provider = prov - end - end - end - - # Look up the current status. This allows us to conventiently look up - # existing status with properties[:foo]. - def properties - if @property_hash.empty? - @property_hash = query || { ensure: :absent } - @property_hash[:ensure] = :absent if @property_hash.empty? - end - @property_hash.dup - end - - # Pull the current state of the list from the full list. We're - # getting some double entendre here.... - def query - self.class.instances.each do |instance| - if instance.name == name || instance.name.downcase == name - return instance.properties - end - end - nil - end -end diff --git a/lib/puppet/provider/firewall/firewall.rb b/lib/puppet/provider/firewall/firewall.rb new file mode 100644 index 000000000..b46976e4b --- /dev/null +++ b/lib/puppet/provider/firewall/firewall.rb @@ -0,0 +1,1088 @@ +# frozen_string_literal: true + +require_relative '../../../puppet_x/puppetlabs/firewall/utility' + +# Implementation for the iptables type using the Resource API. +class Puppet::Provider::Firewall::Firewall + ###### GLOBAL VARIABLES ###### + + # Command to list all chains and rules + # $list_command = 'iptables-save' + $list_command = { + 'IPv4' => 'iptables-save', + 'iptables' => 'iptables-save', + 'IPv6' => 'ip6tables-save', + 'ip6tables' => 'ip6tables-save' + } + # Regex used to divide output of$list_command between tables + $table_regex = %r{(\*(?:nat|mangle|filter|raw|rawpost|broute|security)[^*]+)} + # Regex used to retrieve table name + $table_name_regex = %r{^\*(nat|mangle|filter|raw|rawpost|broute|security)} + # Regex used to retrieve Rules + $rules_regex = %r{(-A.*)\n} + # Base command + $base_command = { + 'IPv4' => 'iptables -t', + 'iptables' => 'iptables -t', + 'IPv6' => 'ip6tables -t', + 'ip6tables' => 'ip6tables -t' + } + # Command to add a rule to a chain + $rule_create_command = '-I' # chain_name rule_num + # Command to update a rule within a chain + $rule_update_command = '-R' # chain_name rule_num + # Command to delete a rule from a chain + $rule_delete_command = '-D' # chain_name rule_num + # Number range 9000-9999 is reserved for unmanaged rules + $unmanaged_rule_regex = %r{^9[0-9]{3}\s.*$} + + # Attribute resource map + # Map is ordered as the attributes appear in the iptables-save/ip6tables-save output + $resource_map = { + chain: '-A', + source: '-s', + destination: '-d', + iniface: '-i', + outiface: '-o', + physdev_in: '--physdev-in', + physdev_out: '--physdev-out', + physdev_is_bridged: '--physdev-is-bridged', + physdev_is_in: '--physdev-is-in', + physdev_is_out: '--physdev-is-out', + proto: '-p', + isfragment: '-f', + isfirstfrag: '-m frag --fragid 0 --fragfirst', + ishasmorefrags: '-m frag --fragid 0 --fragmore', + islastfrag: '-m frag --fragid 0 --fraglast', + stat_mode: '-m statistic --mode', + stat_every: '--every', + stat_packet: '--packet', + stat_probability: '--probability', + src_range: '--src-range', + dst_range: '--dst-range', + tcp_option: '--tcp-option', + tcp_flags: '--tcp-flags', + uid: '--uid-owner', + gid: '--gid-owner', + mac_source: '--mac-source', + sport: ['-m multiport --sports', '--sport'], + dport: ['-m multiport --dports', '--dport'], + src_type: '-m addrtype --src-type', + dst_type: '-m addrtype --dst-type', + socket: '-m socket', + pkttype: '--pkt-type', + ipsec_dir: '--dir', + ipsec_policy: '--pol', + state: '--state', + ctstate: '--ctstate', + ctproto: '--ctproto', + ctorigsrc: '--ctorigsrc', + ctorigdst: '--ctorigdst', + ctreplsrc: '--ctreplsrc', + ctrepldst: '--ctrepldst', + ctorigsrcport: '--ctorigsrcport', + ctorigdstport: '--ctorigdstport', + ctreplsrcport: '--ctreplsrcport', + ctrepldstport: '--ctrepldstport', + ctstatus: '--ctstatus', + ctexpire: '--ctexpire', + ctdir: '--ctdir', + hop_limit: '--hl-eq', + icmp: ['-m icmp --icmp-type', '-m icmp6 --icmpv6-type'], + limit: '--limit', + burst: '--limit-burst', + length: '-m length --length', + recent: '-m recent', + rseconds: '--seconds', + reap: '--reap', + rhitcount: '--hitcount', + rttl: '--rttl', + rname: '--name', + mask: '--mask', + rsource: '--rsource', + rdest: '--rdest', + ipset: '-m set --match-set', + string: '--string', + string_hex: '--hex-string', + string_algo: '--algo', + string_from: '--from', + string_to: '--to', + jump: '-j', + goto: '-g', + clusterip_new: '--new', + clusterip_hashmode: '--hashmode', + clusterip_clustermac: '--clustermac', + clusterip_total_nodes: '--total-nodes', + clusterip_local_node: '--local-node', + clusterip_hash_init: '--hash-init', + queue_num: '--queue-num', + queue_bypass: '--queue-bypass', + nflog_group: '--nflog-group', + nflog_prefix: '--nflog-prefix', + nflog_range: '--nflog-range', + nflog_size: '--nflog-size', + nflog_threshold: '--nflog-threshold', + gateway: '--gateway', + clamp_mss_to_pmtu: '--clamp-mss-to-pmtu', + set_mss: '--set-mss', + set_dscp: '--set-dscp', + set_dscp_class: '--set-dscp-class', + todest: '--to-destination', + tosource: '--to-source', + toports: '--to-ports', + to: '--to', + checksum_fill: '--checksum-fill', + random_fully: '--random-fully', + random: '--random', + log_prefix: '--log-prefix', + log_level: '--log-level', + log_uid: '--log-uid', + log_tcp_sequence: '--log-tcp-sequence', + log_tcp_options: '--log-tcp-options', + log_ip_options: '--log-ip-options', + reject: '--reject-with', + set_mark: '--set-xmark', + match_mark: '-m mark --mark', + mss: '-m tcpmss --mss', + connlimit_upto: '--connlimit-upto', + connlimit_above: '--connlimit-above', + connlimit_mask: '--connlimit-mask', + connmark: '-m connmark --mark', + time_start: '--timestart', + time_stop: '--timestop', + month_days: '--monthdays', + week_days: '--weekdays', + date_start: '--datestart', + date_stop: '--datestop', + time_contiguous: '--contiguous', + kernel_timezone: '--kerneltz', + u32: '--u32', + src_cc: '--source-country', + dst_cc: '--destination-country', + hashlimit_upto: '--hashlimit-upto', + hashlimit_above: '--hashlimit-above', + hashlimit_name: '--hashlimit-name', + hashlimit_burst: '--hashlimit-burst', + hashlimit_mode: '--hashlimit-mode', + hashlimit_srcmask: '--hashlimit-srcmask', + hashlimit_dstmask: '--hashlimit-dstmask', + hashlimit_htable_size: '--hashlimit-htable-size', + hashlimit_htable_max: '--hashlimit-htable-max', + hashlimit_htable_expire: '--hashlimit-htable-expire', + hashlimit_htable_gcinterval: '--hashlimit-htable-gcinterval', + bytecode: '-m bpf --bytecode', + ipvs: '--ipvs', + cgroup: '--cgroup', + rpfilter: '-m rpfilter', + condition: '--condition', + name: '-m comment --comment', + notrack: '--notrack', + helper: '--helper', + zone: '--zone' + } + + # These are known booleans that do not take a value. + $known_booleans = [ + :checksum_fill, :clamp_mss_to_pmtu, :isfragment, :ishasmorefrags, :islastfrag, :isfirstfrag, + :log_uid, :log_tcp_sequence, :log_tcp_options, :log_ip_options, :random_fully, :random, + :rdest, :reap, :rsource, :rttl, :socket, :physdev_is_bridged, :physdev_is_in, :physdev_is_out, + :time_contiguous, :kernel_timezone, :clusterip_new, :queue_bypass, :ipvs, :notrack + ] + + # Properties that use "-m " (with the potential to have multiple + # arguments against the same IPT module) must be in this hash. The keys in this + # hash are the IPT module names, with the values being an array of the respective + # supported arguments for this IPT module. + # + # ** IPT Module arguments must be in order as they would appear in iptables-save ** + # + # Exceptions: + # => multiport: (For some reason, the multiport arguments can't be) + # specified within the same "-m multiport", but works in seperate + # ones. + # => addrtype: Each instance of src_type/dst_type requires it's own preface + # + @module_to_argument_mapping = { + physdev: [:physdev_in, :physdev_out, :physdev_is_bridged, :physdev_is_in, :physdev_is_out], + iprange: [:src_range, :dst_range], + tcp: [:tcp_option, :tcp_flags], + owner: [:uid, :gid], + mac: [:mac_source], + policy: [:ipsec_dir, :ipsec_policy], + condition: [:condition], + pkttype: [:pkttype], + state: [:state], + conntrack: [:ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, + :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir], + hl: [:hop_limit], + limit: [:limit, :burst], + string: [:string, :string_hex, :string_algo, :string_from, :string_to], + connlimit: [:connlimit_upto, :connlimit_above, :connlimit_mask], + time: [:time_start, :time_stop, :month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone], + u32: [:u32], + geoip: [:src_cc, :dst_cc], + hashlimit: [:hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, + :hashlimit_htable_size, :hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval], + ipvs: [:ipvs], + cgroup: [:cgroup] + } + + # This is the order of resources as they appear in ip(6)tables-save output, + # it is used in order to ensure that the rules are applied in the correct order. + # This order can be determined by going through iptables source code or just tweaking and trying manually + $resource_list = [ + :source, :destination, :iniface, :outiface, + :physdev_in, :physdev_out, :physdev_is_bridged, :physdev_is_in, :physdev_is_out, + :proto, :isfragment, :ishasmorefrags, :islastfrag, :isfirstfrag, + :stat_mode, :stat_every, :stat_packet, :stat_probability, + :src_range, :dst_range, :tcp_option, :tcp_flags, :uid, :gid, :mac_source, :sport, :dport, + :src_type, :dst_type, :socket, :pkttype, :ipsec_dir, :ipsec_policy, + :state, :ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, + :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir, + :hop_limit, :icmp, :limit, :burst, :length, :recent, :rseconds, :reap, + :rhitcount, :rttl, :rname, :mask, :rsource, :rdest, :ipset, :string, :string_hex, :string_algo, + :string_from, :string_to, :jump, :goto, :clusterip_new, :clusterip_hashmode, + :clusterip_clustermac, :clusterip_total_nodes, :clusterip_local_node, :clusterip_hash_init, :queue_num, :queue_bypass, + :nflog_group, :nflog_prefix, :nflog_range, :nflog_size, :nflog_threshold, :clamp_mss_to_pmtu, :gateway, + :set_mss, :set_dscp, :set_dscp_class, :todest, :tosource, :toports, :to, :checksum_fill, :random_fully, :random, :log_prefix, + :log_level, :log_uid, :log_tcp_sequence, :log_tcp_options, :log_ip_options, :reject, :set_mark, :match_mark, :mss, + :connlimit_upto, :connlimit_above, :connlimit_mask, :connmark, + :time_start, :time_stop, :month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone, + :u32, :src_cc, :dst_cc, :hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, + :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, :hashlimit_htable_size, + :hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval, + :bytecode, :ipvs, :helper, :zone, :cgroup, :rpfilter, :condition, :name, :notrack + ] + + ###### PUBLIC METHODS ###### + + def get(context) + # Call the private method which returns the rules + # The method is seperated out in this way as it is re-used later in the code + rules = Puppet::Provider::Firewall::Firewall.get_rules(context, false) + # Verify the returned data + Puppet::Provider::Firewall::Firewall.validate_get(context, rules) + # Return array + rules + end + + def set(context, changes) + changes.each do |name, change| + is = change[:is] + should = change[:should] + + is = PuppetX::Firewall::Utility.create_absent(:name, name) if is.nil? + should = PuppetX::Firewall::Utility.create_absent(:name, name) if should.nil? + + # Run static verification against both sets of values + Puppet::Provider::Firewall::Firewall.validate_input(is, should) + # Process the intended values so that they are inputed as they should be + should = Puppet::Provider::Firewall::Firewall.process_input(should) + + if is[:ensure].to_s == 'absent' && should[:ensure].to_s == 'present' + context.creating(name) do + create(context, name, should) + end + elsif is[:ensure].to_s == 'present' && should[:ensure].to_s == 'absent' + context.deleting(name) do + delete(context, name, is) + end + elsif is[:ensure].to_s == 'present' + context.updating(name) do + update(context, name, should) + end + end + end + end + + def create(context, name, should) + context.notice("Creating Rule '#{name}' with #{should.inspect}") + position = Puppet::Provider::Firewall::Firewall.insert_order(context, name, should[:chain], should[:table], should[:protocol]) + arguments = Puppet::Provider::Firewall::Firewall.hash_to_rule(context, name, should) + Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $rule_create_command, should[:chain], position, arguments].join(' ')) + PuppetX::Firewall::Utility.persist_iptables(context, name, should[:protocol]) + end + + def update(context, name, should) + context.notice("Updating Rule '#{name}' with #{should.inspect}") + position = Puppet::Provider::Firewall::Firewall.insert_order(context, name, should[:chain], should[:table], should[:protocol]) + arguments = Puppet::Provider::Firewall::Firewall.hash_to_rule(context, name, should) + Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $rule_update_command, should[:chain], position, arguments].join(' ')) + PuppetX::Firewall::Utility.persist_iptables(context, name, should[:protocol]) + end + + def delete(context, name, is) + context.notice("Deleting Rule '#{name}'") + # When deleting we use the retrieved iptables-save append command as a base + # We do this to ensure accuracy when removing non-standard (i.e. uncommented) rules via the firewallchain purge function + arguments = is[:line].gsub(%r{^-A}, $rule_delete_command) + Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], arguments].join(' ')) + PuppetX::Firewall::Utility.persist_iptables(context, name, is[:protocol]) + end + + # Custom insync method + # Needed for uid and gid + def insync?(context, _name, property_name, is_hash, should_hash) + context.debug("Checking whether '#{property_name}' is out of sync") + + # If either value is nil, no custom logic is required + return nil if is_hash[property_name].nil? || should_hash[property_name].nil? + + case property_name + when :protocol + is = is_hash[property_name] + should = should_hash[property_name] + + # Ensure the should value accurately matches the is + should = 'IPv4' if should == 'iptables' + should = 'IPv6' if should == 'ip6tables' + + is == should + when :source, :destination + # Ensure source/destination has it's valid mask before you compare it + is_hash[property_name] == PuppetX::Firewall::Utility.host_to_mask(should_hash[property_name], should_hash[:protocol]) + when :tcp_option, :ctproto, :hop_limit + # Ensure that the values are compared as strings + is_hash[property_name] == should_hash[property_name].to_s + when :tcp_flags + # Custom logic to account for `ALL` being returned as `FIN,SYN,RST,PSH,ACK,URG` + is = is_hash[property_name].split + should = should_hash[property_name].split + + is = is.map { |x| (x == 'FIN,SYN,RST,PSH,ACK,URG') ? 'ALL' : x } + should = should.map { |x| (x == 'FIN,SYN,RST,PSH,ACK,URG') ? 'ALL' : x } + + is.join(' ') == should.join(' ') + when :uid, :gid + require 'etc' + # The following code allow us to take into consideration unix mappings + # between string usernames and UIDs (integers). We also need to ignore + # spaces as they are irrelevant with respect to rule sync. + + # Remove whitespace + is = is_hash[property_name].to_s.gsub(%r{\s+}, '') + should = should_hash[property_name].to_s.gsub(%r{\s+}, '') + + # Keep track of negation, but remove the '!' + is_negate = '' + should_negate = '' + if is.start_with?('!') + is = is.gsub(%r{^!}, '') + is_negate = '!' + end + if should.start_with?('!') + should = should.gsub(%r{^!}, '') + should_negate = '!' + end + + # If 'is' or 'should' contain anything other than digits or digit range, + # we assume that we have to do a lookup to convert to UID + is = Etc.getpwnam(is).uid unless is[%r{[0-9]+(-[0-9]+)?}] == is + should = Etc.getpwnam(should).uid unless should[%r{[0-9]+(-[0-9]+)?}] == should + + "#{is_negate}#{is}" == "#{should_negate}#{should}" + when :mac_source, :jump + # Value of mac_source/jump may be downcased or upcased when returned depending on the OS + is_hash[property_name].casecmp(should_hash[property_name]).zero? + when :state, :ctstate, :ctstatus + # Ensure that if both is and should are array values, they are correctly compared in order + is = is_hash[property_name] + should = should_hash[property_name] + return nil unless is.is_a?(Array) && should.is_a?(Array) + + if is[0].start_with?('!') + is.append('!') + is[0] = is[0].gsub(%r{^!\s?}, '') + end + if should[0].start_with?('!') + should.append('!') + should[0] = should[0].gsub(%r{^!\s?}, '') + end + is.sort == should.sort + when :icmp + # Ensure that the values are compared to each other as icmp code numbers + is = PuppetX::Firewall::Utility.icmp_name_to_number(is_hash[property_name], is_hash[:protocol]) + should = PuppetX::Firewall::Utility.icmp_name_to_number(should_hash[property_name], should_hash[:protocol]) + is == should + when :log_level + # Ensure that the values are compared to each other as log level numbers + is = PuppetX::Firewall::Utility.log_level_name_to_number(is_hash[property_name]) + should = PuppetX::Firewall::Utility.log_level_name_to_number(should_hash[property_name]) + is == should + when :set_mark + # Ensure that the values are compared to eachother in hexidecimal format + is = PuppetX::Firewall::Utility.mark_mask_to_hex(is_hash[property_name]) + should = PuppetX::Firewall::Utility.mark_mask_to_hex(should_hash[property_name]) + is == should + when :match_mark, :connmark + # Ensure that the values are compared to eachother in hexidecimal format + is = PuppetX::Firewall::Utility.mark_to_hex(is_hash[property_name]) + should = PuppetX::Firewall::Utility.mark_to_hex(should_hash[property_name]) + is == should + when :time_start, :time_stop + # Ensure the values are compared in full `00:00:00` format + is = is_hash[property_name] + should = should_hash[property_name] + + should = "0#{should}" if %r{^([0-9]):}.match?(should) + should = "#{should}:00" if %r{^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$}.match?(should) + + is == should + when :dport, :sport + is = is_hash[property_name] + should = should_hash[property_name] + + # Wrap compared values as arrays in order to simplify comparisons + is = [is] unless is.is_a?(Array) + should = [should] unless should.is_a?(Array) + + # If first value includes a negation, retrieve it and set as it's own value + if is[0].start_with?('!') + is.append('!') + is[0] = is[0].gsub(%r{^!\s?}, '') + end + if should[0].start_with?('!') + should.append('!') + should[0] = should[0].gsub(%r{^!\s?}, '') + end + + # Range can be passed as `-` but will always be set/returned as `:` + # Ensure values are sorted + is.sort == should.map { |port| port.to_s.tr('-', ':') }.sort + when :string_hex + # Compare the values with any whitespace removed + is = is_hash[property_name].to_s.gsub(%r{\s+}, '') + should = should_hash[property_name].to_s.gsub(%r{\s+}, '') + + is == should + else + # Ensure that if both values are arrays, that they are sorted prior to comparison + return nil unless is_hash[property_name].is_a?(Array) && should_hash[property_name].is_a?(Array) + + is_hash[property_name].sort == should_hash[property_name].sort + end + end + + ###### PRIVATE METHODS ###### + ###### GET ###### + + # Retrieve the rules + # Optional values lets you return a simplified set of data, used for determining order when adding/updating/deleting rules, + # while also allowing for the protocols used to retrieve the rules to be limited. + # @api private + def self.get_rules(context, basic, protocols = ['IPv4', 'IPv6']) + # Create empty return array + rules = [] + counter = 1 + # For each protocol + protocols.each do |protocol| + # Retrieve String containing all information + iptables_list = Puppet::Provider.execute($list_command[protocol]) + # Scan String to retrieve all Rules + iptables_list.scan($table_regex).each do |table| + table_name = table[0].scan($table_name_regex)[0][0] + table[0].scan($rules_regex).each do |rule| + raw_rules = if basic + Puppet::Provider::Firewall::Firewall.rule_to_name(context, rule[0], table_name, protocol) + else + Puppet::Provider::Firewall::Firewall.rule_to_hash(context, rule[0], table_name, protocol) + end + # Process the returned values so that it is correct for our purposes + rules << Puppet::Provider::Firewall::Firewall.process_get(context, raw_rules, rule[0], counter) + counter += 1 + end + end + # Return array + end + rules + end + + # Simplified version of 'self.rules_to_hash' meant to return name, chain and table only + # @api private + def self.rule_to_name(_context, rule, table_name, protocol) + rule_hash = {} + rule_hash[:ensure] = 'present' + rule_hash[:table] = table_name + rule_hash[:protocol] = protocol + + name_regex = Regexp.new("#{$resource_map[:name]}\\s(?:\"([^\"]*)|([^\"\\s]*))") + name_value = rule.scan(name_regex)[0] + # Combine the returned values and remove and trailing or leading whitespace + rule_hash[:name] = [name_value[0], name_value[1]].join(' ').strip if name_value + + chain_regex = Regexp.new("#{$resource_map[:chain]}\\s(\\S+)") + rule_hash[:chain] = rule.scan(chain_regex)[0][0] + + rule_hash + end + + # Converts a given rule to a hash value + # @api private + def self.rule_to_hash(_context, rule, table_name, protocol) + # loop through resource map + rule_hash = {} + rule_hash[:ensure] = 'present' + rule_hash[:table] = table_name + rule_hash[:protocol] = protocol + rule_hash[:line] = rule + # Add the ensure parameter first + $resource_map.each do |key, value| + if $known_booleans.include?(key) + # check for flag with regex, add a space/line end to ensure accuracy with the more simplistic flags; i.e. `-f`, `--random` + rule_hash[key] = if rule.match(Regexp.new("#{value}(\\s|$)")) + true + else + false + end + next + end + + case key + when :name, :string, :string_hex, :bytecode, :u32, :nflog_prefix, :log_prefix + # When :name/:string/:string_hex/:bytecode, return everything inside the double quote pair following the key value + # When only a single word comment is returned no quotes are given, so we must check for this as well + # First find if flag is present, add a space to ensure accuracy with the more simplistic flags; i.e. `-i` + if rule.match(Regexp.new("#{value}\\s")) + value_regex = Regexp.new("(?:(!\\s))?#{value}\\s(?:\"([^\"]*)|([^\"\\s]*))") + key_value = rule.scan(value_regex)[0] + # Combine the returned values and remove and trailing or leading whitespace + key_value[1] = [key_value[0], key_value[1], key_value[2]].join + rule_hash[key] = key_value[1] if key_value[1] + end + when :sport, :dport + split_value_regex = value[0].split(%r{ }) + negated_multi_regex = [split_value_regex[0], split_value_regex[1], '!', split_value_regex[2]].join(' ') + if rule.match(value[0]) + # First check against the multiport value, if found split and return as an array + value_regex = Regexp.new("#{value[0]}\\s(\\S+)") + key_value = rule.scan(value_regex)[0] + rule_hash[key] = key_value[0].split(%r{,}) + elsif rule.match(negated_multi_regex) + # Next check against a negated multiport value, if found split and return as an array with the first value negated + value_regex = Regexp.new("#{negated_multi_regex}\\s(\\S+)") + key_value = rule.scan(value_regex)[0] + + # Add '!' to the beginning of the first value to show it as negated + split_value = key_value[0].split(%r{,}) + split_value[0] = "! #{split_value[0]}" + rule_hash[key] = split_value + elsif rule.match(value[1]) + # If no multi value matches, check against the regular value instead + value_regex = Regexp.new("(?:(!)\\s)?#{value[1]}\\s(\\S+)") + key_value = rule.scan(value_regex)[0] + # If it is negated, combine the retrieved '!' with the actual value to make one string + key_value[1] = [key_value[0], key_value[1]].join(' ') unless key_value[0].nil? + rule_hash[key] = key_value[1] + end + when :tcp_flags + # First find if flag is present, add a space to ensure accuracy with the more simplistic flags; i.e. `-i` + if rule.match(Regexp.new("#{value}\\s")) + value_regex = Regexp.new("(?:(!)\\s)?#{value}\\s(\\S+)\\s(\\S+)") + key_value = rule.scan(value_regex)[0] + # If a negation is found combine it with the first retrieved value, then combine both values + key_value[1] = [key_value[0], key_value[1]].join(' ') unless key_value[0].nil? + rule_hash[key] = [key_value[1], key_value[2]].join(' ') + end + when :src_type, :dst_type, :ipset, :match_mark, :mss, :connmark + split_regex = value.split(%r{ }) + if rule.match(Regexp.new("#{split_regex[1]}\\s(?:(!)\\s)?#{split_regex[2]}\\s")) + # The exact information retrieved changes dependeing on the key + type_attr = [:src_type, :dst_type] + value_regex = Regexp.new("#{split_regex[1]}\\s(?:(!)\\s)?#{split_regex[2]}\\s(\\S+)\\s?(--limit-iface-(?:in|out))?") if type_attr.include?(key) + ip_attr = [:ipset] + value_regex = Regexp.new("#{split_regex[1]}\\s(?:(!)\\s)?#{split_regex[2]}\\s(\\S+\\s\\S+)") if ip_attr.include?(key) + mark_attr = [:match_mark, :mss, :connmark] + value_regex = Regexp.new("#{split_regex[1]}\\s(?:(!)\\s)?#{split_regex[2]}\\s(\\S+)") if mark_attr.include?(key) + # Since multiple values can be recovered, we must loop through each instance + type_value = [] + key_value = rule.scan(value_regex) + key_value.length.times do |i| + type_value.append(key_value[i].join(' ').strip) if key_value[i] + end + # If only a single value was found return as a string + rule_hash[key] = type_value[0] if type_value.length == 1 + rule_hash[key] = type_value if type_value.length > 1 + end + when :state, :ctstate, :ctstatus, :month_days, :week_days + if rule.match(Regexp.new("#{value}\\s")) + value_regex = Regexp.new("(?:(!)\\s)?#{value}\\s(\\S+)") + key_value = rule.scan(value_regex) + split_value = key_value[0][1].split(%r{,}) + # If negated add to first value + split_value[0] = [key_value[0][0], split_value[0]].join(' ') unless key_value[0][0].nil? + # If value is meant to be Int, return as such + int_attr = [:month_days] + split_value = split_value.map(&:to_i) if int_attr.include?(key) + # If only a single value is found, strip the Array wrapping + split_value = split_value[0] if split_value.length == 1 + rule_hash[key] = split_value + end + when :icmp + case protocol + when 'IPv4', 'iptables' + proto = 0 + when 'IPv6', 'ip6tables' + proto = 1 + end + + if rule.match(Regexp.new("#{value[proto]}\\s")) + value_regex = Regexp.new("#{value[proto]}\\s(\\S+)") + key_value = rule.scan(value_regex)[0] + rule_hash[key] = key_value[0] + end + when :recent + if rule.match(Regexp.new("#{value}\\s")) + value_regex = Regexp.new("#{value}\\s(!\\s)?--(\\S+)") + key_value = rule.scan(value_regex)[0] + # If it has, combine the retrieved '!' with the actual value to make one string + key_value[1] = [key_value[0], key_value[1]].join unless key_value[0].nil? + rule_hash[key] = key_value[1] if key_value + end + when :rpfilter + if rule.match(Regexp.new("#{value}\\s--")) + # Since the values are their own flags we can simply look for them directly + value_regex = Regexp.new("(?:\s--(invert|validmark|loose|accept-local))") + key_value = rule.scan(value_regex) + return_value = [] + key_value.each do |val| + return_value << val[0] + end + rule_hash[key] = return_value[0] if return_value.length == 1 + rule_hash[key] = return_value if return_value.length > 1 + end + when :proto, :source, :destination, :iniface, :outiface, :physdev_in, :physdev_out, :src_range, :dst_range, + :tcp_option, :uid, :gid, :mac_source, :pkttype, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, + :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctexpire, :cgroup, :hop_limit + # Values where negation is prior to the flag + # First find if flag is present, add a space to ensure accuracy with the more simplistic flags; i.e. `-i` + if rule.match(Regexp.new("#{value}\\s")) + value_regex = Regexp.new("(?:(!)\\s)?#{value}\\s(\\S+)") + key_value = rule.scan(value_regex)[0] + # If it has, combine the retrieved '!' with the actual value to make one string + key_value[1] = [key_value[0], key_value[1]].join(' ') unless key_value[0].nil? + rule_hash[key] = key_value[1] if key_value + end + else # stat_mode, stat_every, stat_packet, stat_probability, socket, ipsec_dir, ipsec_policy, :ctdir, + # :limit, :burst, :length, :rseconds, :rhitcount, :rname, :mask, :string_algo, :string_from, :string_to, + # :jump, :goto, :clusterip_hashmode, :clusterip_clustermac, :clusterip_total_nodes, :clusterip_local_node, + # :clusterip_hash_init, :queue_num, :nflog_group, :nflog_range, :nflog_size, :nflog_threshold, + # :gateway, :set_mss, :set_dscp, :set_dscp_class, :todest, :tosource, :toports, :to, :log_level, + # :reject, :set_mark, :connlimit_upto, :connlimit_above, :connlimit_mask, :time_start, :time_stop, :date_start, + # :date_stop, :src_cc, :dst_cc, :hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, :hashlimit_mode, + # :hashlimit_srcmask, :hashlimit_dstmask, :hashlimit_htable_size, :hashlimit_htable_max, :hashlimit_htable_expire, + # :hashlimit_htable_gcinterval, :zone, :helper, :condition + # Default return, retrieve first complete block following the key value + # First find if flag is present, add a space to ensure accuracy with the more simplistic flags; i.e. `-j`, `--to` + if rule.match(Regexp.new("#{value}\\s")) + value_regex = Regexp.new("#{value}(?:\\s(!)\\s|\\s{1,2})(\\S+)") + key_value = rule.scan(value_regex)[0] + # If it has, combine the retrieved '!' with the actual value to make one string + key_value[1] = [key_value[0], key_value[1]].join(' ') unless key_value[0].nil? + # If value is meant to return as an integer/float ensure it does + int_attr = [:stat_every, :stat_packet, :burst, :rseconds, :rhitcount, :string_from, :string_to, :clusterip_total_nodes, + :clusterip_local_nodes, :nflog_group, :nflog_range, :nflog_size, :nflog_threshold, :set_mss, :connlimit_upto, + :connlimit_above, :connlimit_mask, :hashlimit_burst, :hashlimit_srcmask, :hashlimit_dstmask, :hashlimit_htable_size, + :hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval, :zone] + key_value[1] = key_value[1].to_i if int_attr.include?(key) + if key == :stat_probability && key_value[1].include?('.') + key_value[1] = key_value[1].to_f + elsif key == :stat_probability + key_value[1] = key_value[1].to_i + end + + rule_hash[key] = key_value[1] if key_value + end + end + end + rule_hash + end + + # Verify that the information being retrieved is correct + # @api private + def self.validate_get(_context, rules) + # Verify that names are unique + names = [] + rules.each do |rule| + names << rule[:name] + end + raise ArgumentError, 'Duplicate names have been found within your Firewalls. This prevents the module from working correctly and must be manually resolved.' if names.length != names.uniq.length + # Verify that the current order of the retrieved puppet rules is correct + end + + # Certain attributes need custom logic to ensure that they are returning the correct information + # @api private + def self.process_get(_context, rule_hash, rule, counter) + # Puppet-firewall requires that all rules have structured comments (resource names) and will fail if a + # rule in iptables does not have a matching comment. + if !rule_hash.key?(:name) + num = 9000 + counter + rule_hash[:name] = "#{num} #{Digest::SHA256.hexdigest(rule)}" + elsif !rule_hash[:name].match(%r{(^\d+(?:[ \t-]\S+)+$)}) + num = 9000 + counter + rule_hash[:name] = "#{num} #{rule_hash[:name]}" + end + + # If no specific proto has been set we treat it as having `all` set + rule_hash[:proto] = 'all' unless rule_hash[:proto] + # Certain OS can return the proto as it;s equivalent number and we make sure to convert it in that case + rule_hash[:proto] = PuppetX::Firewall::Utility.proto_number_to_name(rule_hash[:proto]) + + # If a dscp numer is found, also return it as it's valid class name + rule_hash[:set_dscp_class] = PuppetX::Firewall::Utility.dscp_number_to_class(rule_hash[:set_dscp]) if rule_hash[:set_dscp] + + rule_hash + end + + # @api private + def self.create_absent(namevar, title) + result = if title.is_a? Hash + title.dup + else + { namevar => title } + end + result[:ensure] = 'absent' + result + end + + ###### SET ###### + + # Verify that the information being set is correct + # @api private + def self.validate_input(_is, should) + # Verify that name does not start with 9000-9999, this range has been reserved. Ignore check when deleting the rule + raise ArgumentError, 'Rule name cannot start with 9000-9999, as this range is reserved for unmanaged rules.' if should[:name].match($unmanaged_rule_regex) && should[:ensure].to_s == 'present' + # `isfragment` can only be set when `proto` is `tcp` + raise ArgumentError, '`proto` must be set to `tcp` for `isfragment` to be true.' if should[:isfragment] && should[:proto] != 'tcp' + # `stat_mode` must be set to `nth` for `stat_every` and `stat_packet` to be set + raise ArgumentError, '`stat_mode` must be set to `nth` for `stat_every` to be set.' if should[:stat_every] && should[:stat_mode] != 'nth' + raise ArgumentError, '`stat_mode` must be set to `nth` for `stat_packet` to be set.' if should[:stat_packet] && should[:stat_mode] != 'nth' + # `stat_mode` must be set to `random` for `stat_probability` to be set + raise ArgumentError, '`stat_mode` must be set to `random` for `stat_probability` to be set.' if should[:stat_probability] && should[:stat_mode] != 'random' + + # Verify that if dport/sport/state/ctstate/ctstatus is passed as an array, that any negation includes either the first value or al values + [:dport, :sport, :state, :ctstate, :ctstatus].each do |key| + next unless should[key].is_a?(Array) + + negated_values = 0 + should[key].each do |value| + negated_values += 1 if %r{^!\s}.match?(value.to_s) + end + next unless (negated_values == 1 && !should[key][0].to_s.match(%r{^!\s})) || + (negated_values >= 2 && negated_values != should[key].length) + + raise ArgumentError, + "When negating a `#{key}` array, you must negate either the first given value only or all the given values." + end + raise ArgumentError, 'Value `any` is not valid. This behaviour should be achieved by omitting or undefining the ICMP parameter.' if should[:icmp] && should[:icmp] == 'any' + raise ArgumentError, '`burst` cannot be set without `limit`.' if should[:burst] && !(should[:limit]) + + # Verify that a correct range has been passed for `length` + if should[:length] + match = should[:length].to_s.match('^([0-9]+)(?::)?([0-9]+)?$') + low = match[1].to_i + high = match[2].to_i if match[2] + raise ArgumentError, '`length` values must be between 0 and 65535' if (low.negative? || low > 65_535) || (!high.nil? && (high.negative? || high > 65_535 || high < low)) + end + # Recent module + raise ArgumentError, '`recent` must be set to `update` or `rcheck` for `rseconds` to be set.' if should[:rseconds] && (should[:recent] != 'update' && should[:recent] != 'rcheck') + raise ArgumentError, '`rseconds` must be set for `reap` to be set.' if should[:reap] && !should[:rseconds] + raise ArgumentError, '`recent` must be set to `update` or `rcheck` for `rhitcount` to be set.' if should[:rhitcount] && (should[:recent] != 'update' && should[:recent] != 'rcheck') + raise ArgumentError, '`recent` must be set to `update` or `rcheck` for `rttl` to be set.' if should[:rttl] && (should[:recent] != 'update' && should[:recent] != 'rcheck') + raise ArgumentError, '`recent` must be set for `rname` to be set.' if should[:rname] && !should[:recent] + raise ArgumentError, '`recent` must be set for `rsource` to be set.' if should[:rsource] && !should[:recent] + raise ArgumentError, '`recent` must be set for `rdest` to be set.' if should[:rdest] && !should[:recent] + raise ArgumentError, '`rdest` and `rsource` are mutually exclusive, only one may be set at a time.' if should[:rsource] && should[:rdest] + # String module + raise ArgumentError, '`string_algo` must be set for `string` or `string_hex` to be set.' if (should[:string] || should[:string_hex]) && !(should[:string_algo]) + # NFQUEUE + raise ArgumentError, '`queue_num`` must be between 0 and 65535' if should[:queue_num] && (should[:queue_num].to_i > 65_535 || should[:queue_num].to_i.negative?) + # Jump + # `2^16-1` is equal to `65_535` + raise ArgumentError, '`nflog_group` must be between 0 and 2^16-1' if should[:nflog_group] && (should[:nflog_group].to_i > 65_535 || should[:queue_num].to_i.negative?) + raise ArgumentError, 'When setting `jump => TEE`, the gateway property is required' if should[:jump] == 'TEE' && !should[:gateway] + raise ArgumentError, 'When setting `jump => TCPMSS`, the `set_mss` or `clamp_mss_to_pmtu` property is required' if should[:jump] == 'TCPMSS' && !(should[:set_mss] || should[:clamp_mss_to_pmtu]) + raise ArgumentError, 'When setting `jump => DSCP`, the `set_dscp` or `set_dscp_class` property is required' if should[:jump] == 'DSCP' && !(should[:set_dscp] || should[:set_dscp_class]) + raise ArgumentError, 'Parameter `jump => DNAT` only applies to `table => nat`' if should[:jump] == 'DNAT' && should[:table] != 'nat' + raise ArgumentError, 'Parameter `jump => DNAT` must have `todest` parameter' if (should[:jump] == 'DNAT' && !should[:todest]) || (should[:jump] != 'DNAT' && should[:todest]) + raise ArgumentError, 'Parameter `jump => SNAT` only applies to `table => nat`' if should[:jump] == 'SNAT' && should[:table] != 'nat' + raise ArgumentError, 'Parameter `jump => SNAT` must have `tosource` parameter' if (should[:jump] == 'SNAT' && !should[:tosource]) || (should[:jump] != 'SNAT' && should[:tosource]) + raise ArgumentError, 'Parameter `checksum_fill` requires `jump => CHECKSUM` and `table => mangle`' if should[:checksum_fill] && !(should[:jump] == 'CHECKSUM' && should[:table] == 'mangle') + + [:log_prefix, :log_level, :log_uid, :log_tcp_sequence, :log_tcp_options, :log_ip_options].each do |log| + raise ArgumentError, "Parameter `#{log}` requires `jump => LOG`" if should[log] && should[:jump] != 'LOG' + end + raise ArgumentError, 'Parameter `jump => CT` only applies to `table => raw`' if should[:jump] == 'CT' && should[:table] != 'raw' + raise ArgumentError, 'Parameter `zone` requires `jump => CT`' if should[:zone] && should[:jump] != 'CT' + raise ArgumentError, 'Parameter `helper` requires `jump => CT`' if should[:helper] && should[:jump] != 'CT' + raise ArgumentError, 'Parameter `notrack` requires `jump => CT`' if should[:notrack] && should[:jump] != 'CT' + # Connlimit + raise ArgumentError, 'Parameter `connlimit_mask` requires either `connlimit_upto` or `connlimit_above`' if should[:connlimit_mask] && !(should[:connlimit_upto] || should[:connlimit_above]) + + # Hashlimit + [:hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, + :hashlimit_htable_size, :hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval].each do |hash| + next unless should[hash] && (!should[:hashlimit_name] || !(should[:hashlimit_upto] || should[:hashlimit_above])) + + raise ArgumentError, 'Parameter `hashlimit_name` and either `hashlimit_upto` or `hashlimit_above` are required when setting any `hashlimit` attribute.' + end + raise ArgumentError, '`hashlimit_upto` and `hashlimit_above` are mutually exclusive, only one may be set at a time.' if should[:hashlimit_upto] && should[:hashlimit_above] + + # Protocol + ipv4_only = [:clusterip_new, :clusterip_hashmode, :clusterip_clustermac, :clusterip_total_nodes, :clusterip_local_node, :clusterip_hash_init] + ipv4_only.each do |ipv4| + raise ArgumentError, "Parameter `#{ipv4}` is specific to the `IPv4` protocol" if should[ipv4] && !(should[:protocol] == 'IPv4' || should[:protocol] == 'iptables') + end + ipv6_only = [:hop_limit, :ishasmorefrags, :islastfrag, :isfirstfrag] + ipv6_only.each do |ipv6| + raise ArgumentError, "Parameter `#{ipv6}` is specific to the `IPv6` protocol" if should[ipv6] && !(should[:protocol] == 'IPv6' || should[:protocol] == 'ip6tables') + end + ## Array elements must be unique + [:dst_type, :src_type].each do |key| + next unless should[key].is_a?(Array) + raise ArgumentError, "`#{key}` elements must be unique" if should[key].map { |type| type.to_s.gsub(%r{--limit-iface-(in|out)}, '') }.uniq.length != should[key].length + end + # Log prefix size is limited + raise ArgumentError, 'Parameter `nflog_prefix`` must be less than 64 characters' if should[:nflog_prefix] && should[:nflog_prefix].length > 64 + + [:dst_range, :src_range].each do |key| + next unless should[key] + + matches = %r{^([^\-/]+)-([^\-/]+)$}.match(should[key]) + raise(ArgumentError, 'The IP range must be in `IP1-IP2` format.') unless matches + + [matches[1], matches[2]].each do |addr| + begin # rubocop:disable Style/RedundantBegin + PuppetX::Firewall::Utility.host_to_ip(addr, should[:protocol]) + rescue StandardError + raise(ArgumentError, "Invalid IP address `#{addr}` in range `#{should[key]}`") + end + end + end + end + + # Certain attributes need processed in ways that can vary between IPv4 and IPv6 + # @api private + def self.process_input(should) + # `dport`, `sport` `state` `ctstate` and `ctstatus` arrays should only have the first value negated. + [:dport, :sport, :state, :ctstate, :ctstatus].each do |key| + next unless should[key].is_a?(Array) + + negated = true if %r{^!\s}.match?(should[key][0].to_s) + should[key].each_with_index do |_value, _index| + should[key] = should[key].map { |value| value.to_s.tr('! ', '') } + end + should[key][0] = ['!', should[key][0]].join(' ') if negated + end + + # `jump` values should always be uppercase + should[:jump] = should[:jump].upcase if should[:jump] + + # `source` and `destination` must be put through host_to_mask + should[:source] = PuppetX::Firewall::Utility.host_to_mask(should[:source], should[:protocol]) if should[:source] + should[:destination] = PuppetX::Firewall::Utility.host_to_mask(should[:destination], should[:protocol]) if should[:destination] + + # ct attributes must be put through host_to_mask with certain masks then being removed + ct = [:ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst] + ct.each do |c| + break unless should[c] + + value = PuppetX::Firewall::Utility.host_to_mask(should[c], should[:protocol]) + should[c] = if should[:protocol] == 'IPv4' + value.chomp('/32') + else + value.chomp('/128') + end + end + + # `icmp` needs to be converted to a number if passed as a string + should[:icmp] = PuppetX::Firewall::Utility.icmp_name_to_number(should[:icmp], should[:protocol]) if should[:icmp] + + # `log_level` needs to be converted to a number if passed as a string + should[:log_level] = PuppetX::Firewall::Utility.log_level_name_to_number(should[:log_level]) if should[:log_level] + + # `set_mark`, `match_mark` and `connmark` must be applied in hexidecimal format + should[:set_mark] = PuppetX::Firewall::Utility.mark_mask_to_hex(should[:set_mark]) if should[:set_mark] + should[:match_mark] = PuppetX::Firewall::Utility.mark_to_hex(should[:match_mark]) if should[:match_mark] + should[:connmark] = PuppetX::Firewall::Utility.mark_to_hex(should[:connmark]) if should[:connmark] + + # `time_start` and `time_stop` must be applied in full HH:MM:SS format + time = [:time_start, :time_stop] + time.each do |t| + should[t] = "0#{should[t]}" if %r{^([0-9]):}.match?(should[t]) + should[t] = "#{should[t]}:00" if %r{^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$}.match?(should[t]) + end + + # If `sport/dport` range has been passed with `-`, replace with `:` + should[:sport] = should[:sport].to_s.tr('-', ':') if should[:sport].is_a?(String) + should[:dport] = should[:dport].to_s.tr('-', ':') if should[:dport].is_a?(String) + should[:sport] = should[:sport].map { |port| port.to_s.tr('-', ':') } if should[:sport].is_a?(Array) + should[:dport] = should[:dport].map { |port| port.to_s.tr('-', ':') } if should[:dport].is_a?(Array) + + should + end + + # Converts a given hash value to a command line argument + # @api private + def self.hash_to_rule(_context, _name, rule) + arguments = '' + + # We loop through an ordered list of all flags as the order that they are added is important + $resource_list.each do |key| + next unless rule[key] + + value = rule[key] + + # Ensure that the necesary module (`-m`) arguments are added when needed + # addrtype and ipset are exceptions as they need to preface each instance + @module_to_argument_mapping.each do |modules| + # Skip unless the key is part of the module + next unless modules[1].include?(key) + # Skip if the module flag has already been added + next if arguments.match(Regexp.new("-m #{modules[0]}")) + + # Add the module flag + arguments += " -m #{modules[0]}" + end + + # if resource is known_boolean + if $known_booleans.include?(key) + # If value is true, append command to arguments + arguments += " #{$resource_map[key]}" if value + next + end + + # check for existence, retrieve string that follows if it is + # certain resources may need special rules + case key + when :name, :string, :string_hex, :bytecode, :u32, :nflog_prefix, :log_prefix + arguments += " #{[$resource_map[key], "'#{rule[key]}'"].join(' ')}" if rule[key].match?(%r{^[^!]}) # if standard + arguments += " #{['!', $resource_map[key], "'#{rule[key].gsub(%r{^!\s?}, '')}'"].join(' ')}" if rule[key].match?(%r{^!}) # if negated + when :sport, :dport + if rule[key].is_a?(Array) && rule[key][0].to_s.match(%r{^!}) + # Negated Multiport + split_comannd = $resource_map[key][0].split(%r{ }) + negated_command = [split_comannd[0], split_comannd[1], '!', split_comannd[2]].join(' ') + value = rule[key].join(',').gsub(%r{^!\s?}, '') + arguments += " #{[negated_command, value].join(' ')}" + elsif rule[key].is_a?(Array) + # Standard Multiport + arguments += " #{[$resource_map[key][0], rule[key].join(',')].join(' ')}" + elsif rule[key].to_s.match?(%r{^!}) + # Negated Standard + arguments += " #{['!', $resource_map[key][1], rule[key].gsub(%r{^!\s?}, '')].join(' ')}" + else + # Standard + arguments += " #{[$resource_map[key][1], rule[key]].join(' ')}" + end + when :src_type, :dst_type, :ipset, :match_mark, :mss, :connmark + # Code for if value requires it's own flag each time it is applied + split_command = $resource_map[key].split(%r{ }) + negated_command = [split_command[0], split_command[1], '!', split_command[2]].join(' ') + + # If a string, wrap as an array to simplify the code + rule[key] = [rule[key]] if rule[key].is_a?(String) + rule[key].each do |ru| + arguments += " #{$resource_map[key]} #{ru}" unless ru.match?(%r{^!}) + arguments += " #{negated_command} #{ru.gsub(%r{^!\s?}, '')}" if ru.match?(%r{^!}) + end + when :state, :ctstate, :ctstatus, :month_days, :week_days + # Code for if value is an array and all values are joined together and passed as part of a single flag + # If not an array, wrap as an array to simplify the code + rule[key] = [rule[key]] unless rule[key].is_a?(Array) + int_attr = [:month_days] + arguments += " #{[$resource_map[key], rule[key].join(',')].join(' ')}" if int_attr.include?(key) || rule[key][0].match(%r{^[^!]}) # if standard + arguments += " #{['!', $resource_map[key], rule[key].join(',').gsub(%r{^!\s?}, '')].join(' ')}" if !int_attr.include?(key) && rule[key][0].match(%r{^!}) # if negated + when :icmp + case rule[:protocol] + when 'IPv4', 'iptables' + proto = 0 + when 'IPv6', 'ip6tables' + proto = 1 + end + # Retrieve the correct command for the protocol + # A command is generated to be used for negation + split_comannd = $resource_map[key][proto].split(%r{ }) + negated_command = [split_comannd[0], split_comannd[1], '!', split_comannd[2]].join(' ') + + arguments += " #{[$resource_map[key][proto], rule[key]].join(' ')}" if rule[key].match?(%r{^[^!]}) # if standard + arguments += " #{[negated_command, rule[key].gsub(%r{^!\s?}, '')].join(' ')}" if rule[key].match?(%r{^!}) # if negated + when :recent + # Add value after command, if negated add negation before command + # Preface the value of recent with `--` + arguments += " #{$resource_map[key]} --#{rule[key]}" if rule[key].match?(%r{^[^!]}) # if standard + arguments += " #{$resource_map[key]} ! --#{rule[key].gsub(%r{^!\s?}, '')}" if rule[key].match?(%r{^!}) # if negated + when :rpfilter + # Add value after command + # Preface the value of recent with `--` + # If a string, wrap as an array to simplify the code + rule[key] = [rule[key]] if rule[key].is_a?(String) + arguments += " #{$resource_map[key]} --#{rule[key].join(' --')}" + when :proto, :source, :destination, :iniface, :outiface, :physdev_in, :physdev_out, :src_range, :dst_range, + :tcp_option, :tcp_flags, :uid, :gid, :mac_source, :pkttype, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, + :ctrepldst, :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctexpire, :cgroup, :hop_limit + # Add value after command, if negated add negation before command + arguments += " #{[$resource_map[key], rule[key]].join(' ')}" if rule[key].is_a?(Integer) || rule[key].match?(%r{^[^!]}) # if standard + arguments += " #{['!', $resource_map[key], rule[key].gsub(%r{^!\s?}, '')].join(' ')}" if rule[key].is_a?(String) && rule[key].match?(%r{^!}) # if negated + else # :chain, stat_mode, stat_every, stat_packet, stat_probability, socket, ipsec_dir, ipsec_policy, :ctdir, + # :limit, :burst, :length, :rseconds, :rhitcount, :rname, :mask, :string_algo, :string_from, :string_to, + # :jump, :goto, :clusterip_hashmode, :clusterip_clustermac, :clusterip_total_nodes, :clusterip_local_node, + # :clusterip_hash_init, :queue_num, :nflog_group, :nflog_range, :nflog_size, :nflog_threshold, + # :gateway, :set_mss, :set_dscp, :set_dscp_class, :todest, :tosource, :toports, :to, :log_level, + # :reject, :set_mark, :connlimit_upto, :connlimit_above, :connlimit_mask, :time_start, :time_stop, :date_start, + # :date_stop, :src_cc, :dst_cc, :hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, :hashlimit_mode, + # :hashlimit_srcmask, :hashlimit_dstmask, :hashlimit_htable_size, :hashlimit_htable_max, :hashlimit_htable_expire, + # :hashlimit_htable_gcinterval, :zone, :helper, :condition + # Add value after command + arguments += " #{[$resource_map[key], rule[key]].join(' ')}" + end + end + arguments + end + + # Find the correct position for our new rule in its chain + # This has been lifted from the previous provider in order to maintain the logic between them + # @api private + def self.insert_order(context, name, chain, table, protocol) + rules = [] + # Find any rules that match the given chain and table pairing + Puppet::Provider::Firewall::Firewall.get_rules(context, true, [protocol]).each do |rule| + rules << rule[:name] if rule[:chain] == chain && rule[:table] == table + end + + # If no rules found, return 1 + return 1 if rules.empty? + + # Find if this is a new or eisting rule + if rules.include? name + # If the rule already exists, use it as the offset + offset_rule = name + else + # If it doesn't add it to the list and find it's ordered location + rules << name + new_rule_location = rules.sort.uniq.index(name) + offset_rule = if new_rule_location.zero? + # First and only rule + rules[0] + else + # This rule will come after other managed rules, so find the rule + # immediately preceeding it. + rules.sort.uniq[new_rule_location - 1] + end + end + # Count how many unmanaged rules are ahead of the target rule so we know + # how much to add to the insert order + unnamed_offset = rules[0..rules.index(offset_rule)].reduce(0) do |sum, rule| + # This regex matches the names given to unmanaged rules (a number + # 9000-9999 followed by an MD5 hash). + sum + (rule.match($unmanaged_rule_regex) ? 1 : 0) + end + + # We want our rule to come before unmanaged rules if it's not a 9-rule + unnamed_offset -= 1 if offset_rule.match($unmanaged_rule_regex) && !name.match(%r{^9}) + + # Insert our new or updated rule in the correct order of named rules, but + # offset for unnamed rules. + sorted_rules = rules.reject { |r| r.match($unmanaged_rule_regex) }.sort + sorted_rules.index(name) + 1 + unnamed_offset + end +end diff --git a/lib/puppet/provider/firewall/ip6tables.rb b/lib/puppet/provider/firewall/ip6tables.rb deleted file mode 100644 index 14f96d05d..000000000 --- a/lib/puppet/provider/firewall/ip6tables.rb +++ /dev/null @@ -1,331 +0,0 @@ -# frozen_string_literal: true - -Puppet::Type.type(:firewall).provide :ip6tables, parent: :iptables, source: :ip6tables do - @doc = 'Ip6tables type provider' - - has_feature :iptables - has_feature :condition - has_feature :connection_limiting - has_feature :conntrack - has_feature :hop_limiting - has_feature :rate_limiting - has_feature :recent_limiting - has_feature :snat - has_feature :dnat - has_feature :interface_match - has_feature :icmp_match - has_feature :owner - has_feature :state_match - has_feature :reject_type - has_feature :log_level - has_feature :log_prefix - has_feature :log_uid - has_feature :log_tcp_sequence - has_feature :log_tcp_options - has_feature :log_ip_options - has_feature :mark - has_feature :mss - has_feature :nflog_group - has_feature :nflog_prefix - has_feature :nflog_range - has_feature :nflog_threshold - has_feature :tcp_option - has_feature :tcp_flags - has_feature :pkttype - has_feature :ishasmorefrags - has_feature :islastfrag - has_feature :isfirstfrag - has_feature :socket - has_feature :address_type - has_feature :iprange - has_feature :ipsec_dir - has_feature :ipsec_policy - has_feature :mask - has_feature :ipset - has_feature :length - has_feature :string_matching - has_feature :queue_num - has_feature :queue_bypass - has_feature :ct_target - - optional_commands(ip6tables: 'ip6tables', - ip6tables_save: 'ip6tables-save') - - confine kernel: :linux - - const_set(:Ip6tables_version, Facter.value('ip6tables_version')) - mark_flag = if const_get(:Ip6tables_version) && Puppet::Util::Package.versioncmp(const_get(:Ip6tables_version), '1.4.1') < 0 - '--set-mark' - else - '--set-xmark' - end - - kernelversion = Facter.value('kernelversion') - if (kernelversion && Puppet::Util::Package.versioncmp(kernelversion, '3.13') >= 0) && - (const_get(:Ip6tables_version) && Puppet::Util::Package.versioncmp(const_get(:Ip6tables_version), '1.6.2') >= 0) - has_feature :random_fully - end - - if (kernelversion && Puppet::Util::Package.versioncmp(kernelversion, '3.3') >= 0) && - (const_get(:Ip6tables_version) && Puppet::Util::Package.versioncmp(const_get(:Ip6tables_version), '1.4.13') >= 0) - has_feature :rpfilter - end - - if const_get(:Ip6tables_version) && Puppet::Util::Package.versioncmp(const_get(:Ip6tables_version), '1.6.1') >= 0 - has_feature :nflog_size - end - - def initialize(*args) - raise ArgumentError, 'The ip6tables provider is not supported on version 1.3 of iptables' if Puppet::Type::Firewall::ProviderIp6tables::Ip6tables_version&.match(%r{1\.3\.\d}) - super - end - - def self.iptables(*args) - ip6tables(*args) - end - - def self.iptables_save(*args) - ip6tables_save(*args) - end - - @protocol = 'IPv6' - - @resource_map = { - burst: '--limit-burst', - checksum_fill: '--checksum-fill', - clamp_mss_to_pmtu: '--clamp-mss-to-pmtu', - condition: '--condition', - connlimit_above: '-m connlimit --connlimit-above', - connlimit_mask: '--connlimit-mask', - connmark: '-m connmark --mark', - ctstate: '--ctstate', - ctproto: '--ctproto', - ctorigsrc: '--ctorigsrc', - ctorigdst: '--ctorigdst', - ctreplsrc: '--ctreplsrc', - ctrepldst: '--ctrepldst', - ctorigsrcport: '--ctorigsrcport', - ctorigdstport: '--ctorigdstport', - ctreplsrcport: '--ctreplsrcport', - ctrepldstport: '--ctrepldstport', - ctstatus: '--ctstatus', - ctexpire: '--ctexpire', - ctdir: '--ctdir', - destination: '-d', - dport: ['-m multiport --dports', '--dport'], - dst_range: '--dst-range', - dst_type: '--dst-type', - gateway: '--gateway', - gid: '--gid-owner', - goto: '-g', - hop_limit: '-m hl --hl-eq', - icmp: '-m icmp6 --icmpv6-type', - iniface: '-i', - ipsec_dir: '-m policy --dir', - ipsec_policy: '--pol', - ipset: '-m set --match-set', - isfirstfrag: '-m frag --fragid 0 --fragfirst', - ishasmorefrags: '-m frag --fragid 0 --fragmore', - islastfrag: '-m frag --fragid 0 --fraglast', - jump: '-j', - length: '-m length --length', - limit: '-m limit --limit', - log_level: '--log-level', - log_prefix: '--log-prefix', - log_uid: '--log-uid', - log_tcp_sequence: '--log-tcp-sequence', - log_tcp_options: '--log-tcp-options', - log_ip_options: '--log-ip-options', - mask: '--mask', - match_mark: '-m mark --mark', - name: '-m comment --comment', - mac_source: ['-m mac --mac-source', '--mac-source'], - mss: '-m tcpmss --mss', - nflog_group: '--nflog-group', - nflog_prefix: '--nflog-prefix', - nflog_range: '--nflog-range', - nflog_size: '--nflog-size', - nflog_threshold: '--nflog-threshold', - outiface: '-o', - pkttype: '-m pkttype --pkt-type', - port: '-m multiport --ports', - proto: '-p', - queue_num: '--queue-num', - queue_bypass: '--queue-bypass', - random_fully: '--random-fully', - rdest: '--rdest', - reap: '--reap', - recent: '-m recent', - reject: '--reject-with', - rhitcount: '--hitcount', - rname: '--name', - rpfilter: '-m rpfilter', - rseconds: '--seconds', - rsource: '--rsource', - rttl: '--rttl', - set_dscp: '--set-dscp', - set_dscp_class: '--set-dscp-class', - set_mark: mark_flag, - set_mss: '--set-mss', - socket: '-m socket', - source: '-s', - sport: ['-m multiport --sports', '--sport'], - src_range: '--src-range', - src_type: '--src-type', - stat_every: '--every', - stat_mode: '-m statistic --mode', - stat_packet: '--packet', - stat_probability: '--probability', - state: '-m state --state', - string: '-m string --string', - string_hex: '-m string --hex-string', - string_algo: '--algo', - string_from: '--from', - string_to: '--to', - table: '-t', - tcp_option: '--tcp-option', - tcp_flags: '--tcp-flags', - todest: '--to-destination', - toports: '--to-ports', - tosource: '--to-source', - uid: '--uid-owner', - physdev_in: '--physdev-in', - physdev_out: '--physdev-out', - physdev_is_bridged: '--physdev-is-bridged', - physdev_is_in: '--physdev-is-in', - physdev_is_out: '--physdev-is-out', - date_start: '--datestart', - date_stop: '--datestop', - time_start: '--timestart', - time_stop: '--timestop', - month_days: '--monthdays', - week_days: '--weekdays', - time_contiguous: '--contiguous', - kernel_timezone: '--kerneltz', - src_cc: '--source-country', - dst_cc: '--destination-country', - hashlimit_name: '--hashlimit-name', - hashlimit_upto: '--hashlimit-upto', - hashlimit_above: '--hashlimit-above', - hashlimit_burst: '--hashlimit-burst', - hashlimit_mode: '--hashlimit-mode', - hashlimit_srcmask: '--hashlimit-srcmask', - hashlimit_dstmask: '--hashlimit-dstmask', - hashlimit_htable_size: '--hashlimit-htable-size', - hashlimit_htable_max: '--hashlimit-htable-max', - hashlimit_htable_expire: '--hashlimit-htable-expire', - hashlimit_htable_gcinterval: '--hashlimit-htable-gcinterval', - bytecode: '-m bpf --bytecode', - zone: '--zone', - helper: '--helper', - notrack: '--notrack', - } - - # These are known booleans that do not take a value, but we want to munge - # to true if they exist. - @known_booleans = [ - :checksum_fill, - :clamp_mss_to_pmtu, - :ishasmorefrags, - :islastfrag, - :isfirstfrag, - :log_uid, - :log_tcp_sequence, - :log_tcp_options, - :log_ip_options, - :random_fully, - :rsource, - :rdest, - :reap, - :rpfilter, - :rttl, - :socket, - :physdev_is_bridged, - :physdev_is_in, - :physdev_is_out, - :time_contiguous, - :kernel_timezone, - :queue_bypass, - :notrack, - ] - - # Properties that use "-m " (with the potential to have multiple - # arguments against the same IPT module) must be in this hash. The keys in this - # hash are the IPT module names, with the values being an array of the respective - # supported arguments for this IPT module. - # - # ** IPT Module arguments must be in order as they would appear in iptables-save ** - # - # Exceptions: - # => multiport: (For some reason, the multiport arguments can't be) - # specified within the same "-m multiport", but works in seperate - # ones. - # - @module_to_argument_mapping = { - physdev: [:physdev_in, :physdev_out, :physdev_is_bridged, :physdev_is_in, :physdev_is_out], - addrtype: [:src_type, :dst_type], - iprange: [:src_range, :dst_range], - owner: [:uid, :gid], - condition: [:condition], - conntrack: [:ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, - :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir], - time: [:time_start, :time_stop, :month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone], - geoip: [:src_cc, :dst_cc], - hashlimit: [:hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, - :hashlimit_htable_size, :hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval], - - } - - # Create property methods dynamically - (@resource_map.keys << :chain << :table << :action).each do |property| - if @known_booleans.include?(property) - # The boolean properties default to '' which should be read as false - define_method property.to_s do - @property_hash[property] = :false if @property_hash[property].nil? - @property_hash[property.to_sym] - end - else - define_method property.to_s do - @property_hash[property.to_sym] - end - end - - if property == :chain - define_method "#{property}=" do |value| - if @property_hash[:chain] != value - raise ArgumentError, 'Modifying the chain for existing rules is not supported.' - end - end - else - define_method "#{property}=" do |_value| - @property_hash[:needs_change] = true - end - end - end - - # This is the order of resources as they appear in iptables-save output, - # we need it to properly parse and apply rules, if the order of resource - # changes between puppet runs, the changed rules will be re-applied again. - # This order can be determined by going through iptables source code or just tweaking and trying manually - # (Note: on my CentOS 6.4 ip6tables-save returns -m frag on the place - # I put it when calling the command. So compability with manual changes - # not provided with current parser [georg.koester]) - @resource_list = [:table, :source, :destination, :iniface, :outiface, :physdev_in, - :physdev_out, :physdev_is_bridged, :physdev_is_in, :physdev_is_out, - :proto, :ishasmorefrags, :islastfrag, :isfirstfrag, :src_range, :dst_range, - :tcp_option, :tcp_flags, :uid, :gid, :mac_source, :sport, :dport, :port, :src_type, - :dst_type, :socket, :pkttype, :ipsec_dir, :ipsec_policy, :state, - :ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, - :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir, - :icmp, :hop_limit, :limit, :burst, :length, :recent, :rseconds, :reap, - :rhitcount, :rttl, :rname, :mask, :rsource, :rdest, :ipset, :string, :string_hex, :string_algo, - :string_from, :string_to, :jump, - :nflog_group, :nflog_prefix, :nflog_range, :nflog_size, :nflog_threshold, - :clamp_mss_to_pmtu, :gateway, :todest, - :tosource, :toports, :checksum_fill, :log_level, :log_prefix, :log_uid, :log_tcp_sequence, :log_tcp_options, :log_ip_options, :random_fully, - :reject, :set_mss, :set_dscp, :set_dscp_class, :mss, :queue_num, :queue_bypass, - :set_mark, :match_mark, :connlimit_above, :connlimit_mask, :connmark, :time_start, :time_stop, :month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone, - :src_cc, :dst_cc, :hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, - :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, :hashlimit_htable_size, - :hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval, :bytecode, :zone, :helper, :rpfilter, :condition, :name, :notrack] -end diff --git a/lib/puppet/provider/firewall/iptables.rb b/lib/puppet/provider/firewall/iptables.rb deleted file mode 100644 index 6dcef3492..000000000 --- a/lib/puppet/provider/firewall/iptables.rb +++ /dev/null @@ -1,1004 +0,0 @@ -# frozen_string_literal: true - -require 'puppet/provider/firewall' -require 'digest' - -Puppet::Type.type(:firewall).provide :iptables, parent: Puppet::Provider::Firewall do - include Puppet::Util::Firewall - - @doc = 'Iptables type provider' - - has_feature :iptables - has_feature :condition - has_feature :connection_limiting - has_feature :conntrack - has_feature :rate_limiting - has_feature :recent_limiting - has_feature :snat - has_feature :dnat - has_feature :netmap - has_feature :interface_match - has_feature :icmp_match - has_feature :owner - has_feature :state_match - has_feature :reject_type - has_feature :log_level - has_feature :log_prefix - has_feature :log_uid - has_feature :log_tcp_sequence - has_feature :log_tcp_options - has_feature :log_ip_options - has_feature :mark - has_feature :mss - has_feature :nflog_group - has_feature :nflog_prefix - has_feature :nflog_range - has_feature :nflog_threshold - has_feature :tcp_option - has_feature :tcp_flags - has_feature :pkttype - has_feature :isfragment - has_feature :socket - has_feature :address_type - has_feature :iprange - has_feature :ipsec_dir - has_feature :ipsec_policy - has_feature :mask - has_feature :ipset - has_feature :clusterip - has_feature :length - has_feature :string_matching - has_feature :queue_num - has_feature :queue_bypass - has_feature :ipvs - has_feature :ct_target - - optional_commands(iptables: 'iptables', - iptables_save: 'iptables-save') - - defaultfor kernel: :linux - confine kernel: :linux - - iptables_version = Facter.value('iptables_version') - mark_flag = if iptables_version && Puppet::Util::Package.versioncmp(iptables_version, '1.4.1') < 0 - '--set-mark' - else - '--set-xmark' - end - - kernelversion = Facter.value('kernelversion') - if (kernelversion && Puppet::Util::Package.versioncmp(kernelversion, '3.13') >= 0) && - (iptables_version && Puppet::Util::Package.versioncmp(iptables_version, '1.6.2') >= 0) - has_feature :random_fully - end - - if (kernelversion && Puppet::Util::Package.versioncmp(kernelversion, '3.3') >= 0) && - (iptables_version && Puppet::Util::Package.versioncmp(iptables_version, '1.4.13') >= 0) - has_feature :rpfilter - end - - if iptables_version && Puppet::Util::Package.versioncmp(iptables_version, '1.6.1') >= 0 - has_feature :nflog_size - end - - @protocol = 'IPv4' - - @resource_map = { - burst: '--limit-burst', - checksum_fill: '--checksum-fill', - clamp_mss_to_pmtu: '--clamp-mss-to-pmtu', - condition: '--condition', - connlimit_above: '-m connlimit --connlimit-above', - connlimit_mask: '--connlimit-mask', - connmark: '-m connmark --mark', - ctstate: '--ctstate', - ctproto: '--ctproto', - ctorigsrc: '--ctorigsrc', - ctorigdst: '--ctorigdst', - ctreplsrc: '--ctreplsrc', - ctrepldst: '--ctrepldst', - ctorigsrcport: '--ctorigsrcport', - ctorigdstport: '--ctorigdstport', - ctreplsrcport: '--ctreplsrcport', - ctrepldstport: '--ctrepldstport', - ctstatus: '--ctstatus', - ctexpire: '--ctexpire', - ctdir: '--ctdir', - destination: '-d', - dport: ['-m multiport --dports', '--dport'], - dst_range: '--dst-range', - dst_type: '--dst-type', - gateway: '--gateway', - gid: '--gid-owner', - icmp: '-m icmp --icmp-type', - iniface: '-i', - ipsec_dir: '-m policy --dir', - ipsec_policy: '--pol', - ipset: '-m set --match-set', - isfragment: '-f', - jump: '-j', - goto: '-g', - length: '-m length --length', - limit: '-m limit --limit', - log_level: '--log-level', - log_prefix: '--log-prefix', - log_uid: '--log-uid', - log_tcp_sequence: '--log-tcp-sequence', - log_tcp_options: '--log-tcp-options', - log_ip_options: '--log-ip-options', - mac_source: ['-m mac --mac-source', '--mac-source'], - mask: '--mask', - match_mark: '-m mark --mark', - mss: '-m tcpmss --mss', - name: '-m comment --comment', - nflog_group: '--nflog-group', - nflog_prefix: '--nflog-prefix', - nflog_range: '--nflog-range', - nflog_size: '--nflog-size', - nflog_threshold: '--nflog-threshold', - outiface: '-o', - pkttype: '-m pkttype --pkt-type', - port: '-m multiport --ports', - proto: '-p', - queue_num: '--queue-num', - queue_bypass: '--queue-bypass', - random_fully: '--random-fully', - random: '--random', - rdest: '--rdest', - reap: '--reap', - recent: '-m recent', - reject: '--reject-with', - rhitcount: '--hitcount', - rname: '--name', - rpfilter: '-m rpfilter', - rseconds: '--seconds', - rsource: '--rsource', - rttl: '--rttl', - set_dscp: '--set-dscp', - set_dscp_class: '--set-dscp-class', - set_mark: mark_flag, - set_mss: '--set-mss', - socket: '-m socket', - source: '-s', - sport: ['-m multiport --sports', '--sport'], - src_range: '--src-range', - src_type: '--src-type', - stat_every: '--every', - stat_mode: '-m statistic --mode', - stat_packet: '--packet', - stat_probability: '--probability', - state: '-m state --state', - string: '-m string --string', - string_hex: '-m string --hex-string', - string_algo: '--algo', - string_from: '--from', - string_to: '--to', - table: '-t', - tcp_option: '--tcp-option', - tcp_flags: '--tcp-flags', - todest: '--to-destination', - toports: '--to-ports', - tosource: '--to-source', - to: '--to', - uid: '--uid-owner', - u32: ['-m u32 --u32', '--u32'], - physdev_in: '--physdev-in', - physdev_out: '--physdev-out', - physdev_is_bridged: '--physdev-is-bridged', - physdev_is_in: '--physdev-is-in', - physdev_is_out: '--physdev-is-out', - date_start: '--datestart', - date_stop: '--datestop', - time_start: '--timestart', - time_stop: '--timestop', - month_days: '--monthdays', - week_days: '--weekdays', - time_contiguous: '--contiguous', - kernel_timezone: '--kerneltz', - clusterip_new: '--new', - clusterip_hashmode: '--hashmode', - clusterip_clustermac: '--clustermac', - clusterip_total_nodes: '--total-nodes', - clusterip_local_node: '--local-node', - clusterip_hash_init: '--hash-init', - src_cc: '--source-country', - dst_cc: '--destination-country', - hashlimit_name: '--hashlimit-name', - hashlimit_upto: '--hashlimit-upto', - hashlimit_above: '--hashlimit-above', - hashlimit_burst: '--hashlimit-burst', - hashlimit_mode: '--hashlimit-mode', - hashlimit_srcmask: '--hashlimit-srcmask', - hashlimit_dstmask: '--hashlimit-dstmask', - hashlimit_htable_size: '--hashlimit-htable-size', - hashlimit_htable_max: '--hashlimit-htable-max', - hashlimit_htable_expire: '--hashlimit-htable-expire', - hashlimit_htable_gcinterval: '--hashlimit-htable-gcinterval', - bytecode: '-m bpf --bytecode', - ipvs: '-m ipvs --ipvs', - zone: '--zone', - helper: '--helper', - cgroup: '-m cgroup --cgroup', - notrack: '--notrack', - } - - # These are known booleans that do not take a value, but we want to munge - # to true if they exist. - @known_booleans = [ - :checksum_fill, - :clamp_mss_to_pmtu, - :isfragment, - :log_uid, - :log_tcp_sequence, - :log_tcp_options, - :log_ip_options, - :random_fully, - :random, - :rdest, - :reap, - :rsource, - :rttl, - :socket, - :physdev_is_bridged, - :physdev_is_in, - :physdev_is_out, - :time_contiguous, - :kernel_timezone, - :clusterip_new, - :queue_bypass, - :ipvs, - :notrack, - ] - - # Properties that use "-m " (with the potential to have multiple - # arguments against the same IPT module) must be in this hash. The keys in this - # hash are the IPT module names, with the values being an array of the respective - # supported arguments for this IPT module. - # - # ** IPT Module arguments must be in order as they would appear in iptables-save ** - # - # Exceptions: - # => multiport: (For some reason, the multiport arguments can't be) - # specified within the same "-m multiport", but works in seperate - # ones. - # - @module_to_argument_mapping = { - physdev: [:physdev_in, :physdev_out, :physdev_is_bridged, :physdev_is_in, :physdev_is_out], - addrtype: [:src_type, :dst_type], - iprange: [:src_range, :dst_range], - owner: [:uid, :gid], - condition: [:condition], - conntrack: [:ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, - :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir], - time: [:time_start, :time_stop, :month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone], - geoip: [:src_cc, :dst_cc], - hashlimit: [:hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, - :hashlimit_htable_size, :hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval], - } - - def self.munge_resource_map_from_existing_values(resource_map_original, compare) - resource_map_new = resource_map_original.clone - - @module_to_argument_mapping.each do |ipt_module, arg_array| - arg_array.each do |argument| - if resource_map_original[argument].is_a?(Array) - if compare.include?(resource_map_original[argument].first) - resource_map_new[argument] = resource_map_original[argument].clone - resource_map_new[argument][0] = "-m #{ipt_module} #{resource_map_original[argument].first}" - break - end - elsif compare.include?(resource_map_original[argument] + ' ') - resource_map_new[argument] = "-m #{ipt_module} #{resource_map_original[argument]}" - break - end - end - end - resource_map_new - end - - def munge_resource_map_from_resource(resource_map_original, compare) - resource_map_new = resource_map_original.clone - # We ignore the 'tcp' match module on rule parse, but it needs to be included to generate - # arguments, since both '--tcp-option' and '--tcp-flags' should be prefixed with the match - # module spec. '--dport' and '--sport' are ignored because we only produce multiport-style - # portspecs, which do not require '-m tcp', and for which iptables-save does not include - # '-m tcp' in its output. - tcp_module_arguments = { tcp: [:tcp_option, :tcp_flags] } - module_to_argument_mapping = self.class.instance_variable_get('@module_to_argument_mapping').merge(tcp_module_arguments) - - module_to_argument_mapping.each do |ipt_module, arg_array| - arg_array.each do |argument| - next unless compare[argument] - if resource_map_original[argument].is_a?(Array) - resource_map_new[argument] = resource_map_original[argument].clone - resource_map_new[argument][0] = "-m #{ipt_module} #{resource_map_original[argument].first}" - else - resource_map_new[argument] = "-m #{ipt_module} #{resource_map_original[argument]}" - end - break - end - end - resource_map_new - end - - # Create property methods dynamically - (@resource_map.keys << :chain << :table << :action).each do |property| - if @known_booleans.include?(property) - # The boolean properties default to '' which should be read as false - define_method property.to_s do - @property_hash[property] = :false if @property_hash[property].nil? - @property_hash[property.to_sym] - end - else - define_method property.to_s do - @property_hash[property.to_sym] - end - end - - if property == :chain - define_method "#{property}=" do |value| - if @property_hash[:chain] != value - raise ArgumentError, 'Modifying the chain for existing rules is not supported.' - end - end - else - define_method "#{property}=" do |_value| - @property_hash[:needs_change] = true - end - end - end - - # This is the order of resources as they appear in iptables-save output, - # we need it to properly parse and apply rules, if the order of resource - # changes between puppet runs, the changed rules will be re-applied again. - # This order can be determined by going through iptables source code or just tweaking and trying manually - @resource_list = [ - :table, :source, :destination, :iniface, :outiface, - :physdev_in, :physdev_out, :physdev_is_bridged, :physdev_is_in, :physdev_is_out, - :proto, :isfragment, :stat_mode, :stat_every, :stat_packet, :stat_probability, - :src_range, :dst_range, :tcp_option, :tcp_flags, :uid, :gid, :mac_source, :sport, :dport, :port, - :src_type, :dst_type, :socket, :pkttype, :ipsec_dir, :ipsec_policy, - :state, :ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, - :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir, - :icmp, :limit, :burst, :length, :recent, :rseconds, :reap, - :rhitcount, :rttl, :rname, :mask, :rsource, :rdest, :ipset, :string, :string_hex, :string_algo, - :string_from, :string_to, :jump, :goto, :clusterip_new, :clusterip_hashmode, - :clusterip_clustermac, :clusterip_total_nodes, :clusterip_local_node, :clusterip_hash_init, :queue_num, :queue_bypass, - :nflog_group, :nflog_prefix, :nflog_range, :nflog_size, :nflog_threshold, :clamp_mss_to_pmtu, :gateway, - :set_mss, :set_dscp, :set_dscp_class, :todest, :tosource, :toports, :to, :checksum_fill, :random_fully, :random, :log_prefix, - :log_level, :log_uid, :log_tcp_sequence, :log_tcp_options, :log_ip_options, :reject, :set_mark, :match_mark, :mss, :connlimit_above, :connlimit_mask, :connmark, :time_start, :time_stop, - :month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone, - :src_cc, :dst_cc, :hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, - :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, :hashlimit_htable_size, - :hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval, :bytecode, :ipvs, :zone, :helper, :cgroup, :rpfilter, :condition, :name, :notrack - ] - - def insert - debug 'Inserting rule %s' % resource[:name] - iptables insert_args - end - - def update - debug 'Updating rule %s' % resource[:name] - iptables update_args - end - - def delete - debug 'Deleting rule %s' % resource[:name] - begin - iptables delete_args - rescue Puppet::ExecutionFailure => e - # There is currently a bug in ip6tables where delete rules do not match rules using any protocol - # if '-p' all is missing. - # - # https://bugzilla.netfilter.org/show_bug.cgi?id=1015 - # - # This tries deleting again with -p all to see if that helps. - # - if self.class.instance_variable_get(:@protocol) == 'IPv6' && properties[:proto] == 'all' - begin - iptables delete_args.concat(['-p', 'all']) - rescue Puppet::ExecutionFailure => e # rubocop:disable Lint/SuppressedException - end - end - - # Check to see if the iptables rule is already gone. This can sometimes - # happen as a side effect of other resource changes. If it's not gone, - # raise the error as per usual. - raise e unless resource.property(:ensure).insync?(:absent) - - # If it's already gone, there is no error. Still record a change, but - # adjust the change message to indicate ambiguity over what work Puppet - # actually did to remove the resource, vs. what could have been a side - # effect of something else puppet did. - resource.property(:ensure).singleton_class.send(:define_method, :change_to_s) do |_a, _b| - 'ensured absent' - end - end - end - - def exists? - properties[:ensure] != :absent - end - - # Flush the property hash once done. - def flush - debug('[flush]') - if @property_hash.delete(:needs_change) - notice('Properties changed - updating rule') - update - end - persist_iptables(self.class.instance_variable_get(:@protocol)) - @property_hash.clear - end - - def self.instances - debug '[instances]' - table = nil - rules = [] - # Used for detecting duplicate rules - duplicate_rules = [] - counter = 1 - - # String#lines would be nice, but we need to support Ruby 1.8.5 - nf_warning_msg = "# Warning: ip6?tables-legacy tables present, use ip6?tables-legacy-save to see them\n" - iptables_save.gsub(%r{#{nf_warning_msg}}, '').split("\n").each do |line| - unless %r{^\#\s+|^\:\S+|^COMMIT|^FATAL}.match?(line) - if %r{^\*}.match?(line) - table = line.sub(%r{\*}, '') - else - hash = rule_to_hash(line, table, counter) - if hash - raise "Duplicate rule found for #{hash[:name]}. Skipping update." if duplicate_rules.include?(hash[:name]) - duplicate_rules << hash[:name] - rules << new(hash) - counter += 1 - end - end - end - end - rules - end - - def self.rule_to_hash(line, table, counter) - hash = {} - keys = [] - values = line.dup - - #################### - # PRE-PARSE CLUDGING - #################### - - # The match for ttl - values = values.gsub(%r{(!\s+)?-m ttl (!\s+)?--ttl-(eq|lt|gt) [0-9]+}, '') - # --tcp-flags takes two values; we cheat by adding " around it - # so it behaves like --comment - values = values.gsub(%r{(!\s+)?--tcp-flags (\S*) (\S*)}, '--tcp-flags "\1\2 \3"') - # --hex-string output is in quotes, need to move ! inside quotes - values = values.gsub(%r{(!\s+)?--hex-string "(\S*?)"}, '--hex-string "\1\2"') - # --condition output is in quotes, need to move ! inside quotes - values.gsub!(%r{(!\s+)?--condition "(\S*?)"}, '--condition "\1\2"') - # --match-set can have multiple values with weird iptables format - if %r{-m set (!\s+)?--match-set}.match?(values) - values = values.gsub(%r{(!\s+)?--match-set (\S*) (\S*)}, '--match-set \1\2 \3') - ind = values.index('-m set --match-set') - sets = values.scan(%r{-m set --match-set ((?:!\s+)?\S* \S*)}) - values = values.gsub(%r{-m set --match-set (!\s+)?\S* \S* }, '') - values.insert(ind, "-m set --match-set \"#{sets.join(';')}\" ") - end - # --comment can have multiple values, the same as --match-set - if %r{-m comment --comment}.match?(values) - ind = values.index('-m comment --comment') - comments = values.scan(%r{-m comment --comment "((?:\\"|[^"])*)"}) - comments += values.scan(%r{-m comment --comment ([^"\s]+)\b}) - values = values.gsub(%r{-m comment --comment (".*?[^\\"]")( |$)}, '') - values = values.gsub(%r{-m comment --comment ([^"].*?)[ $]}, '') - values.insert(ind, "-m comment --comment \"#{comments.join(';')}\" ") - end - if %r{-m addrtype (!\s+)?--src-type}.match?(values) - values = values.gsub(%r{(!\s+)?--src-type (\S*)(\s--limit-iface-(in|out))?}, '--src-type \1\2\3') - ind = values.index('-m addrtype --src-type') - types = values.scan(%r{-m addrtype --src-type ((?:!\s+)?\S*(?: --limit-iface-(?:in|out))?)}) - values = values.gsub(%r{-m addrtype --src-type ((?:!\s+)?\S*(?: --limit-iface-(?:in|out))?) ?}, '') - values.insert(ind, "-m addrtype --src-type \"#{types.join(';')}\" ") - end - if %r{-m addrtype (!\s+)?--dst-type}.match?(values) - values = values.gsub(%r{(!\s+)?--dst-type (\S*)(\s--limit-iface-(in|out))?}, '--dst-type \1\2\3') - ind = values.index('-m addrtype --dst-type') - types = values.scan(%r{-m addrtype --dst-type ((?:!\s+)?\S*(?: --limit-iface-(?:in|out))?)}) - values = values.gsub(%r{-m addrtype --dst-type ((?:!\s+)?\S*(?: --limit-iface-(?:in|out))?) ?}, '') - values.insert(ind, "-m addrtype --dst-type \"#{types.join(';')}\" ") - end - # the actual rule will have the ! mark before the option. - values = values.gsub(%r{(!)\s*(-\S+)\s*(\S*)}, '\2 "\1 \3"') unless values.include?('--physdev') - # we do a similar thing for negated address masks (source and destination). - values = values.gsub(%r{(?<=\s)(-\S+) (!)\s?(\S*)}, '\1 "\2 \3"') - # fix negated physdev rules - values = values.gsub(%r{-m physdev ! (--physdev-is-\S+)}, '-m physdev \1 "!"') - # The match extensions for tcp & udp are implied by the protocol, cannot be - # given unless the protocol matches the extension name, are never required, - # and if multiport matches are used, may not even be in the iptables-save - # output in predictable locations. They need to be removed to preserve - # parse sanity. - values = values.gsub(%r{-m (tcp|udp) }, '') - # There is a bug in EL5 which puts 2 spaces before physdev, so we fix it - values = values.gsub(%r{\s{2}--physdev}, ' --physdev') - # '--pol ipsec' takes many optional arguments; we cheat again by adding " around them - values = values.sub(%r{ - --pol\sipsec - (\s--strict)? - (\s--reqid\s\S+)? - (\s--spi\s\S+)? - (\s--proto\s\S+)? - (\s--mode\s\S+)? - (\s--tunnel-dst\s\S+)? - (\s--tunnel-src\s\S+)? - (\s--next)?}x, - '--pol "ipsec\1\2\3\4\5\6\7\8" ') - - # rpfilter also takes multiple parameters; use quote trick again - rpfilter_opts = values.scan(%r{-m\srpfilter(\s(--loose)|\s(--validmark)|\s(--accept-local)|\s(--invert))+}) - if rpfilter_opts && rpfilter_opts.length == 1 && rpfilter_opts[0] - rpfilter_opts = rpfilter_opts[0][1..-1].reject { |x| x.nil? } - values = values.sub( - %r{-m\srpfilter(\s(--loose)|\s(--validmark)|\s(--accept-local)|\s(--invert))+}, - "-m rpfilter \"#{rpfilter_opts.join(' ')}\"", - ) - end - - # on some iptables versions, --connlimit-saddr switch is added after the rule is applied - values = values.gsub(%r{--connlimit-saddr}, '') - - resource_map = munge_resource_map_from_existing_values(@resource_map, values) - - # Trick the system for booleans - @known_booleans.each do |bool| - # append "true" because all params are expected to have values - values = if bool == :isfragment - # -f requires special matching: - # only replace those -f that are not followed by an l to - # distinguish between -f and the '-f' inside of --tcp-flags. - values.sub(%r{\s-f(?!l)(?=.*--comment)}, ' -f true') - elsif resource_map[bool].eql?(%r{'--physdev-is-\S+'}) - values.sub(%r{'#{resource_map[bool]} "! "'}, "#{resource_map[bool]} true") - else - # append `true` to booleans that are not already negated (followed by "!") - values.sub(%r{#{resource_map[bool]}(?=\s|$)(?!\s?"!")}, "#{resource_map[bool]} true") - end - end - - ############ - # Populate parser_list with used value, in the correct order - ############ - map_index = {} - resource_map.each_pair do |map_k, map_v| - [map_v].flatten.each do |v| - ind = values.index(%r{\s#{v}\s}) - next unless ind - map_index[map_k] = ind - end - end - # Generate parser_list based on the index of the found option - parser_list = [] - map_index.sort_by { |_k, v| v }.each { |mapi| parser_list << mapi.first } - - ############ - # MAIN PARSE - ############ - - # Here we iterate across our values to generate an array of keys - parser_list.reverse_each do |k| - resource_map_key = resource_map[k] - [resource_map_key].flatten.each do |opt| - if values.slice!(%r{\s#{opt}}) - keys << k - break - end - end - end - - # Manually remove chain - if %r{(\s|^)-A\s}.match?(values) - values = values.sub(%r{(\s|^)-A\s}, '\1') - keys << :chain - end - - # Manually remove table (used in some tests) - if %r{^-t\s}.match?(values) - values = values.sub(%r{^-t\s}, '') - keys << :table - end - - # manually remove comments if they made it this far - if %r{-m comment --comment}.match?(values) - values = values.sub(%r{-m comment --comment "((?:\\"|[^"])*)"}, {}) - end - - valrev = values.scan(%r{("([^"\\]|\\.)*"|\S+)}).transpose[0].reverse - - if keys.length != valrev.length - warning "Skipping unparsable iptables rule: keys (#{keys.length}) and values (#{valrev.length}) count mismatch on line: #{line}" - return - end - - # Here we generate the main hash by scanning arguments off the values - # string, handling any quoted characters present in the value, and then - # zipping the values with the array of keys. - keys.zip(valrev) do |f, v| - hash[f] = if %r{^".*"$}.match?(v) - v.sub(%r{^"(.*)"$}, '\1').gsub(%r{\\(\\|'|")}, '\1') - else - v.dup - end - end - - ##################### - # POST PARSE CLUDGING - ##################### - - [:dport, :sport, :port, :state, :ctstate, :ctstatus].each do |prop| - hash[prop] = hash[prop].split(',') unless hash[prop].nil? - end - - [:ipset, :dst_type, :src_type].each do |prop| - hash[prop] = hash[prop].split(';') unless hash[prop].nil? - end - - hash[:rpfilter] = hash[:rpfilter].split(' ') unless hash[:rpfilter].nil? - - ## clean up DSCP class to HEX mappings - valid_dscp_classes = { - '0x0a' => 'af11', - '0x0c' => 'af12', - '0x0e' => 'af13', - '0x12' => 'af21', - '0x14' => 'af22', - '0x16' => 'af23', - '0x1a' => 'af31', - '0x1c' => 'af32', - '0x1e' => 'af33', - '0x22' => 'af41', - '0x24' => 'af42', - '0x26' => 'af43', - '0x08' => 'cs1', - '0x10' => 'cs2', - '0x18' => 'cs3', - '0x20' => 'cs4', - '0x28' => 'cs5', - '0x30' => 'cs6', - '0x38' => 'cs7', - '0x2e' => 'ef', - } - [:set_dscp_class].each do |prop| - [:set_dscp].each do |dmark| - next unless hash[dmark] - hash[prop] = valid_dscp_classes[hash[dmark]] - end - end - - # Convert booleans removing the previous cludge we did - @known_booleans.each do |bool| - unless [nil, 'true', '!'].include?(hash[bool]) - raise "Parser error: #{bool} was meant to be a boolean but received value: #{hash[bool]}." - end - end - - # Our type prefers hyphens over colons for ranges so ... - # Iterate across all ports replacing colons with hyphens so that ranges match - # the types expectations. - [:dport, :sport, :port].each do |prop| - next unless hash[prop] - hash[prop] = hash[prop].map do |elem| - elem.tr(':', '-') - end - end - hash[:length]&.tr!(':', '-') - - # Invert any rules that are prefixed with a '!' - [ - :connmark, - :condition, - :ctstate, - :ctproto, - :ctorigsrc, - :ctorigdst, - :ctreplsrc, - :ctrepldst, - :ctorigsrcport, - :ctorigdstport, - :ctreplsrcport, - :ctrepldstport, - :ctstatus, - :ctexpire, - :destination, - :dport, - :dst_range, - :port, - :physdev_is_bridged, - :physdev_is_in, - :physdev_is_out, - :proto, - :source, - :sport, - :src_range, - :state, - ].each do |prop| - if hash[prop]&.is_a?(Array) - # find if any are negated, then negate all if so - should_negate = hash[prop].index do |value| - value.match(%r{^(!)\s+}) - end - if should_negate - hash[prop] = hash[prop].map do |v| - "! #{v.sub(%r{^!\s+}, '')}" - end - end - elsif hash[prop] - m = hash[prop].match(%r{^(!?)\s?(.*)}) - neg = '! ' if m[1] == '!' - hash[prop] = if [:source, :destination].include?(prop) - # Normalise all rules to CIDR notation. - "#{neg}#{Puppet::Util::IPCidr.new(m[2]).cidr}" - else - "#{neg}#{m[2]}" - end - end - end - - # States should always be sorted. This ensures that the output from - # iptables-save and user supplied resources is consistent. - hash[:state] = hash[:state].sort unless hash[:state].nil? - hash[:ctstate] = hash[:ctstate].sort unless hash[:ctstate].nil? - hash[:ctstatus] = hash[:ctstatus].sort unless hash[:ctstatus].nil? - - # This forces all existing, commentless rules or rules with invalid comments to be moved - # to the bottom of the stack. - # Puppet-firewall requires that all rules have comments (resource names) and match this - # regex and will fail if a rule in iptables does not have a comment. We get around this - # by appending a high level - if !hash[:name] - num = 9000 + counter - hash[:name] = "#{num} #{Digest::SHA256.hexdigest(line)}" - elsif not %r{^\d+[[:graph:][:space:]]+$} =~ hash[:name] # rubocop:disable Style/Not : Making this change breaks the code - num = 9000 + counter - hash[:name] = "#{num} #{%r{([[:graph:][:space:]]+)}.match(hash[:name])[1]}" - end - - # Iptables defaults to log_level '4', so it is omitted from the output of iptables-save. - # If the :jump value is LOG and you don't have a log-level set, we assume it to be '4'. - if hash[:jump] == 'LOG' && !hash[:log_level] - hash[:log_level] = '4' - end - - # Iptables defaults to burst '5', so it is ommitted from the output of iptables-save. - # If the :limit value is set and you don't have a burst set, we assume it to be '5'. - if hash[:limit] && !hash[:burst] - hash[:burst] = '5' - end - - hash[:line] = line - hash[:provider] = name.to_s - hash[:table] = table - hash[:ensure] = :present - - # Munge some vars here ... - - # Proto should equal 'all' if undefined - hash[:proto] = 'all' unless hash.include?(:proto) - - # If the jump parameter is set to one of: ACCEPT, REJECT or DROP then - # we should set the action parameter instead. - if ['ACCEPT', 'REJECT', 'DROP'].include?(hash[:jump]) - hash[:action] = hash[:jump].downcase - hash.delete(:jump) - end - hash - end - - def insert_args - args = [] - args << ['-I', resource[:chain], insert_order] - args << general_args - args - end - - def update_args - args = [] - args << ['-R', resource[:chain], insert_order] - args << general_args - args - end - - def delete_args - # Split into arguments - line = properties[:line].gsub(%r{^\-A }, '-D ').split(%r{\s+(?=(?:[^"]|"[^"]*")*$)}).map { |v| v.gsub(%r{^"}, '').gsub(%r{"$}, '') } - line.unshift('-t', properties[:table]) - end - - # This method takes the resource, and attempts to generate the command line - # arguments for iptables. - def general_args - debug 'Current resource: %s' % resource.class - - args = [] - resource_list = self.class.instance_variable_get('@resource_list') - known_booleans = self.class.instance_variable_get('@known_booleans') - resource_map = self.class.instance_variable_get('@resource_map') - resource_map = munge_resource_map_from_resource(resource_map, resource) - - # Always attempt to wait for a lock for iptables to prevent failures when - # puppet is running at the same time something else is managing the rules - # note: --wait wasn't added untip iptables version 1.4.20 - iptables_version = Facter.value('iptables_version') - if iptables_version && Puppet::Util::Package.versioncmp(iptables_version, '1.4.20') >= 0 - args << ['--wait'] - end - - # nflog options are not available on older OSes - [:nflog_group, :nflog_prefix, :nflog_threshold, :nflog_range].each do |nflog_feature| - raise "#{nflog_feature} is not available on iptables version #{iptables_version}" if resource[nflog_feature] && (iptables_version && iptables_version < '1.3.7') - end - - [:dst_type, :src_type].each do |prop| - next unless resource[prop] - - resource[prop].each do |type| - if type =~ %r{--limit-iface-(in|out)} && (iptables_version && iptables_version < '1.4.1') - raise '--limit-iface-in and --limit-iface-out are available from iptables version 1.4.1' - end - end - - raise "Multiple #{prop} elements are available from iptables version 1.4.1" if resource[prop].length > 1 && (iptables_version && iptables_version < '1.4.1') - raise "#{prop} elements must be unique" if resource[prop].map { |type| type.to_s.gsub(%r{--limit-iface-(in|out)}, '') }.uniq.length != resource[prop].length - end - - complex_args = [:ipset, :dst_type, :src_type] - - resource_list.each do |res| - resource_value = nil - if resource[res] - resource_value = resource[res] - # If socket is true then do not add the value as -m socket is standalone - if known_booleans.include?(res) - next unless resource[res] == :true && known_booleans.include?(res) - resource_value = nil - end - elsif res == :jump && resource[:action] - # In this case, we are substituting jump for action - resource_value = resource[:action].to_s.upcase - else - next - end - - args << [resource_map[res]].flatten.first.split(' ') - args = args.flatten - - # On negations, the '!' has to be before the option (eg: "! -d 1.2.3.4") - if resource_value.is_a?(String) && resource_value.start_with?('!') - resource_value = resource_value.sub(%r{^!\s*}, '') - # we do this after adding the 'dash' argument because of ones like "-m multiport --dports", where we want it before the "--dports" but after "-m multiport". - # so we insert before whatever the last argument is - args.insert(-2, '!') - elsif resource_value.is_a?(Symbol) && resource_value.to_s.match(%r{^!}) - resource_value = resource_value.to_s.sub!(%r{^!\s*}, '').to_sym - args.insert(-2, '!') - elsif resource_value.is_a?(Array) && !complex_args.include?(res) - - should_negate = resource_value.index do |value| - value.to_s.match(%r{^(!)\s+}) - end - if should_negate - resource_value, wrong_values = resource_value.map { |value| - if value.is_a?(String) - wrong = value unless %r{^!\s+}.match?(value) - [value.sub(%r{^!\s*}, ''), wrong] - else - [value, nil] - end - }.transpose - wrong_values = wrong_values.compact - unless wrong_values.empty? - raise "All values of the '#{res}' property must be prefixed with a '!' when inverting, but " \ - "'#{wrong_values.join("', '")}' #{(wrong_values.length > 1) ? 'are' : 'is'} not prefixed; aborting" - end - args.insert(-2, '!') - # rubocop:enable Metrics/BlockNesting - end - end - - # For sport and dport, convert hyphens to colons since the type - # expects hyphens for ranges of ports. - if [:sport, :dport, :port].include?(res) - resource_value = resource_value.map do |elem| - elem.tr('-', ':') - end - end - - # ipset can accept multiple values with weird iptables arguments - if complex_args.include?(res) - - resource_value.join(" #{[resource_map[res]].flatten.first} ").split(' ').each do |a| - if a.sub!(%r{^!\s*}, '') - # Negate ipset options - args.insert(-2, '!') - end - - args << a unless a.empty? - end - # our tcp_flags takes a single string with comma lists separated - # by space - # --tcp-flags expects two arguments - elsif res == :tcp_flags - one, two = resource_value.split(' ') - args << one - args << two - elsif res == :rpfilter - args << resource_value - elsif resource_value.is_a?(Array) - args << resource_value.join(',') - elsif !resource_value.nil? - args << resource_value - end - end - - args - end - - def insert_order - debug('[insert_order]') - rules = [] - - # Find list of current rules based on chain and table - self.class.instances.each do |rule| - if rule.chain == resource[:chain].to_s && rule.table == resource[:table].to_s - rules << rule.name - end - end - - # No rules at all? Just bail now. - return 1 if rules.empty? - - # Add our rule to the end of the array of known rules - my_rule = resource[:name].to_s - rules << my_rule - - unmanaged_rule_regex = %r{^9[0-9]{3}\s.*$} - # Find if this is a new rule or an existing rule, then find how many - # unmanaged rules preceed it. - if rules.length == rules.uniq.length - # This is a new rule so find its ordered location. - new_rule_location = rules.sort.uniq.index(my_rule) - offset_rule = if new_rule_location.zero? - # The rule will be the first rule in the chain because nothing came - # before it. - rules[0] - else - # This rule will come after other managed rules, so find the rule - # immediately preceeding it. - rules.sort.uniq[new_rule_location - 1] - end - else - # This is a pre-existing rule, so find the offset from the original - # ordering. - offset_rule = my_rule - end - # Count how many unmanaged rules are ahead of the target rule so we know - # how much to add to the insert order - unnamed_offset = rules[0..rules.index(offset_rule)].reduce(0) do |sum, rule| - # This regex matches the names given to unmanaged rules (a number - # 9000-9999 followed by an MD5 hash). - sum + (rule.match(unmanaged_rule_regex) ? 1 : 0) - end - - # We want our rule to come before unmanaged rules if it's not a 9-rule - if offset_rule.match(unmanaged_rule_regex) && !my_rule.match(%r{^9}) - unnamed_offset -= 1 - end - - # Insert our new or updated rule in the correct order of named rules, but - # offset for unnamed rules. - sorted_rules = rules.reject { |r| r.match(unmanaged_rule_regex) }.sort - raise 'Rule sorting error. Make sure that the title of your rule does not start with 9000-9999, as this range is reserved.' if sorted_rules.index(my_rule).nil? - sorted_rules.index(my_rule) + 1 + unnamed_offset - end -end diff --git a/lib/puppet/provider/firewallchain/firewallchain.rb b/lib/puppet/provider/firewallchain/firewallchain.rb new file mode 100644 index 000000000..b4c6dfbe2 --- /dev/null +++ b/lib/puppet/provider/firewallchain/firewallchain.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +require_relative '../../../puppet_x/puppetlabs/firewall/utility' + +# Implementation for the firewallchain type using the Resource API. +class Puppet::Provider::Firewallchain::Firewallchain + ###### GLOBAL VARIABLES ###### + + # Command to list all chains and rules + $list_command = { + 'IPv4' => 'iptables-save', + 'IPv6' => 'ip6tables-save' + } + # Regex used to divide output of$list_command between tables + $table_regex = %r{(\*(?:nat|mangle|filter|raw|rawpost|broute|security)[^*]+)} + # Regex used to retrieve table name + $table_name_regex = %r{^\*(nat|mangle|filter|raw|rawpost|broute|security)} + # Regex used to retrieve Chains + $chain_regex = %r{\n:(INPUT|FORWARD|OUTPUT|(?:\S+))(?:\s(ACCEPT|DROP|QEUE|RETURN|PREROUTING|POSTROUTING))?} + # Base commands for the protocols, including table affixes + $base_command = { + 'IPv4' => 'iptables -t', + 'IPv6' => 'ip6tables -t' + } + # Command to create a chain + $chain_create_command = '-N' + # Command to flush all rules from a chain, must be used before deleting + $chain_flush_command = '-F' + # Command to delete a chain, cannot be used on inbuilt + $chain_delete_command = '-X' + # Command to set chain policy, works on inbuilt chains only + $chain_policy_command = '-P' + # Check if the given chain name references a built in one + $built_in_regex = %r{^(?:INPUT|OUTPUT|FORWARD|PREROUTING|POSTROUTING)$} + + ###### PUBLIC METHODS ###### + + # Raw data is retrieved via `iptables-save` and then regexed to retrieve the different Chains and whether they have a set Policy + def get(_context) + # Create empty return array + chains = [] + # Scan String to retrieve all Chains and Policies + ['IPv4', 'IPv6'].each do |protocol| + # Retrieve String containing all IPv4 information + iptables_list = Puppet::Provider.execute($list_command[protocol]) + iptables_list.scan($table_regex).each do |table| + table_name = table[0].scan($table_name_regex)[0][0] + table[0].scan($chain_regex).each do |chain| + # Create the base hash + chain_hash = { + name: "#{chain[0]}:#{table_name}:#{protocol}", + purge: false, + ignore_foreign: false, + ensure: 'present' + } + # If a policy was found add to the hash + chain_hash[:policy] = chain[1].downcase if chain[1] + chains << chain_hash + end + end + end + # Return array + chains + end + + def set(context, changes) + changes.each do |name, change| + is = change[:is] + should = change[:should] + + is = PuppetX::Firewall::Utility.create_absent(:name, name) if is.nil? + should = PuppetX::Firewall::Utility.create_absent(:name, name) if should.nil? + + # Process the input and divide the name into it's relevant parts + is, should = Puppet::Provider::Firewallchain::Firewallchain.process_input(is, should) + # Run static verification against both sets of values + Puppet::Provider::Firewallchain::Firewallchain.verify(is, should) + + if is[:ensure].to_s == 'absent' && should[:ensure].to_s == 'present' + context.creating(name) do + create(context, name, should) + end + elsif is[:ensure].to_s == 'present' && should[:ensure].to_s == 'absent' + context.deleting(name) do + delete(context, name, is) + end + elsif is[:ensure].to_s == 'present' && should[:ensure].to_s == 'present' + context.updating(name) do + update(context, name, should, is) + end + end + end + end + + def create(context, name, should) + context.notice("Creating Chain '#{name}' with #{should.inspect}") + Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $chain_create_command, should[:chain]].join(' ')) + PuppetX::Firewall::Utility.persist_iptables(context, name, should[:protocol]) + end + + def update(context, name, should, is) + # Skip the update if not a inbuilt chain or if policy has not been updated + return if !$built_in_regex.match(should[:chain]) || + ($built_in_regex.match(should[:chain]) && is[:policy] == should[:policy]) + + context.notice("Updating Chain '#{name}' with #{should.inspect}") + Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $chain_policy_command, should[:chain], should[:policy].upcase].join(' ')) + PuppetX::Firewall::Utility.persist_iptables(context, name, should[:protocol]) + end + + def delete(context, name, is) + # Before we can delete a chain we must first flush it of any active rules + context.notice("Flushing Chain '#{name}'") + Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_flush_command, is[:chain]].join(' ')) + + # For Inbuilt chains we cannot delete them and so instead simply ensure they are reverted to the default policy + if $built_in_regex.match(is[:chain]) + context.notice("Reverting Internal Chain '#{name}' to its default") + Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_policy_command, is[:chain], 'ACCEPT'].join(' ')) + else + context.notice("Deleting Chain '#{name}'") + Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_delete_command, is[:chain]].join(' ')) + end + PuppetX::Firewall::Utility.persist_iptables(context, name, is[:protocol]) + end + + # Custom insync method + def insync?(context, _name, property_name, _is_hash, _should_hash) + context.debug("Checking whether '#{property_name}' is out of sync") + + case property_name + when :purge, :ignore, :ignore_foreign + # Suppres any update notifications for the purge/ignore(_foreign) values + # They are used in the generate method which is ran prior to this point and have no + # bearing on it's actual state. + true + else + nil + end + end + + ###### PRIVATE METHODS ###### + + # Process the information so that it can be correctly applied + # @api private + def self.process_input(is, should) + # Split the name into it's relevant parts + is[:name] = is[:title] if is[:name].nil? + is[:chain], is[:table], is[:protocol] = is[:name].split(':') + should[:name] = should[:title] if should[:name].nil? + should[:chain], should[:table], should[:protocol] = should[:name].split(':') + + # If an in-built chain, always treat it as being present and ensure it is assigned a policy + # The retrieval of in-built chains may get confused by `iptables-save` tendency to not return table information + # for tables that have not yet been interacted with. + is[:ensure] = 'present' if $built_in_regex.match(is[:chain]) + is[:policy] = 'accept' if $built_in_regex.match(is[:chain]) && is[:policy].nil? + # For the same reason assign it the default policy as an intended state if it does not have one + should[:policy] = 'accept' if $built_in_regex.match(should[:chain]) && should[:policy].nil? + + [is, should] + end + + # Verify that the information is correct + # @api private + def self.verify(_is, should) + # Verify that no incorrect chain names are passed + case should[:table] + when 'filter' + raise ArgumentError, 'INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table \'filter\'' if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(should[:chain]) + when 'mangle' + raise ArgumentError, 'PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table \'mangle\'' if %r{^(BROUTING)$}.match?(should[:chain]) + when 'nat' + raise ArgumentError, 'PREROUTING, POSTROUTING, INPUT, and OUTPUT are the only inbuilt chains that can be used in table \'nat\'' if %r{^(BROUTING|FORWARD)$}.match?(should[:chain]) + raise ArgumentError, 'table nat isn\'t valid in IPv6. You must specify \':IPv4\' as the name suffix' if %r{^(IP(v6)?)?$}.match?(should[:protocol]) + when 'raw' + raise ArgumentError, 'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\'' if %r{^(POSTROUTING|BROUTING|INPUT|FORWARD)$}.match?(should[:chain]) + when 'broute' + raise ArgumentError, 'BROUTE is only valid with protocol \'ethernet\'' if should[:protocol] != 'ethernet' + raise ArgumentError, 'BROUTING is the only inbuilt chain allowed on on table \'broute\'' if %r{^PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT$}.match?(should[:chain]) + when 'security' + raise ArgumentError, 'INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table \'security\'' if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(should[:chain]) + end + + # Verify that Policy is only passed for the inbuilt chains + raise ArgumentError, "'policy' can only be set on Internal Chains. Setting for '#{should[:name]}' is invalid" if !$built_in_regex.match(should[:chain]) && should.key?(:policy) + + # Warn that inbuilt chains will be flushed, not deleted + warn "Warning: Inbuilt Chains may not be deleted. Chain `#{should[:name]}` will be flushed and have it's policy reverted to default." if $built_in_regex.match(should[:chain]) && + should[:ensure] == 'absent' + end + + # Customer generate method called by the resource_api + # Finds and returns all unmanaged rules on the chain that are not set to be ignored + def generate(_context, title, _is, should) + # Unless purge is true, return an empty array + return [] unless should[:purge] + + # gather a list of all rules present on the system + rules_resources = Puppet::Type.type(:firewall).instances + + # Retrieve information from the title + name, table, protocol = title.split(':') + + # Keep only rules in this chain + rules_resources.delete_if do |resource| + resource.rsapi_current_state[:chain] != name || resource.rsapi_current_state[:table] != table || resource.rsapi_current_state[:protocol] != protocol + end + + # Remove rules which match our ignore filter + # Ensure ignore value is wrapped as an array to simplify the code + should[:ignore] = [should[:ignore]] if should[:ignore].is_a?(String) + rules_resources.delete_if { |resource| should[:ignore].find_index { |ignore| resource.rsapi_current_state[:line].match(ignore) } } if should[:ignore] + + # Remove rules that were (presumably) not put in by puppet + rules_resources.delete_if { |resource| resource.rsapi_current_state[:name].match(%r{^(\d+)[[:graph:][:space:]]})[1].to_i >= 9000 } if should[:ignore_foreign] + + # We mark all remaining rules for deletion, and then let the catalog override us on rules which should be present + # We also ensure that the generate rules have the correct protocol to avoid issues with our validation + rules_resources.each do |resource| + resource[:ensure] = :absent + resource[:protocol] = protocol + end + + rules_resources + end +end diff --git a/lib/puppet/provider/firewallchain/iptables_chain.rb b/lib/puppet/provider/firewallchain/iptables_chain.rb deleted file mode 100644 index 6abe9fbd5..000000000 --- a/lib/puppet/provider/firewallchain/iptables_chain.rb +++ /dev/null @@ -1,180 +0,0 @@ -# frozen_string_literal: true - -Puppet::Type.type(:firewallchain).provide :iptables_chain do - include Puppet::Util::Firewall - - @doc = 'Iptables chain provider' - - has_feature :iptables_chain - has_feature :policy - - optional_commands(iptables: 'iptables', - iptables_save: 'iptables-save', - ip6tables: 'ip6tables', - ip6tables_save: 'ip6tables-save', - ebtables: 'ebtables', - ebtables_save: 'ebtables-save') - - defaultfor kernel: :linux - confine kernel: :linux - - # chain name is greedy so we anchor from the end. - # [\d+:\d+] doesn't exist on ebtables - MAPPING = { - IPv4: { - tables: method(:iptables), - save: method(:iptables_save), - re: %r{^:(.+)\s(\S+)\s\[\d+:\d+\]$}, - }, - IPv6: { - tables: method(:ip6tables), - save: method(:ip6tables_save), - re: %r{^:(.+)\s(\S+)\s\[\d+:\d+\]$}, - }, - ethernet: { - tables: method(:ebtables), - save: method(:ebtables_save), - re: %r{^:(.+)\s(\S+)$}, - }, - }.freeze - INTERNAL_CHAINS = %r{^(PREROUTING|POSTROUTING|BROUTING|INPUT|FORWARD|OUTPUT)$}.freeze - TABLES = 'nat|mangle|filter|raw|rawpost|broute|security' - NAME_FORMAT = %r{^(.+):(#{TABLES}):(IP(v[46])?|ethernet)$}.freeze - - def create - allvalidchains do |t, chain, table, protocol| - if INTERNAL_CHAINS.match?(chain) - # can't create internal chains - warning "Attempting to create internal chain #{@resource[:name]}" - end - if properties[:ensure] == protocol - debug "Skipping Inserting chain #{chain} on table #{table} (#{protocol}) already exists" - else - debug "Inserting chain #{chain} on table #{table} (#{protocol}) using #{t}" - t.call ['-t', table, '-N', chain] - unless @resource[:policy].nil? - t.call ['-t', table, '-P', chain, @resource[:policy].to_s.upcase] - end - end - end - end - - def destroy - allvalidchains do |t, chain, table, protocol| - if INTERNAL_CHAINS.match?(chain) - # can't delete internal chains - warning "Attempting to destroy internal chain #{@resource[:name]}" - else - debug "Flush chain #{chain} on table #{table} (#{protocol})" - t.call ['-t', table, '-F', chain] - debug "Deleting chain #{chain} on table #{table} (#{protocol})" - t.call ['-t', table, '-X', chain] - end - end - end - - def exists? - allvalidchains do |_t, chain| - if INTERNAL_CHAINS.match?(chain) - # If the chain isn't present, it's likely because the module isn't loaded. - # If this is true, then we fall into 2 cases - # 1) It'll be loaded on demand - # 2) It won't be loaded on demand, and we throw an error - # This is the intended behavior as it's not the provider's job to load kernel modules - # So we pretend it exists... - return true - end - end - properties[:ensure] == :present - end - - def policy=(value) - return if value == :empty - allvalidchains do |t, chain, table| - p = ['-t', table, '-P', chain, value.to_s.upcase] - debug "[set policy] #{t} #{p}" - t.call p - end - end - - def policy - debug "[get policy] #{@resource[:name]} =#{@property_hash[:policy].to_s.downcase}" - @property_hash[:policy].to_s.downcase - end - - def self.prefetch(resources) - debug('[prefetch(resources)]') - instances.each do |prov| - resource = resources[prov.name] - if resource - resource.provider = prov - end - end - end - - def flush - debug('[flush]') - persist_iptables(@resource[:name].match(NAME_FORMAT)[3]) - # Clear the property hash so we re-initialize with updated values - @property_hash.clear - end - - # Look up the current status. This allows us to conventiently look up - # existing status with properties[:foo]. - def properties - if @property_hash.empty? - @property_hash = query || { ensure: :absent } - end - @property_hash.dup - end - - # Pull the current state of the list from the full list. - def query - self.class.instances.each do |instance| - if instance.name == name - debug "query found #{name}" % instance.properties.inspect - return instance.properties - end - end - nil - end - - def self.instances - debug '[instances]' - table = nil - chains = [] - - MAPPING.each do |p, c| - begin # rubocop:disable Style/RedundantBegin - c[:save].call.each_line do |line| - if line =~ c[:re] - name = Regexp.last_match(1) + ':' + ((table == 'filter') ? 'filter' : table) + ':' + p.to_s - policy = (Regexp.last_match(2) == '-') ? nil : Regexp.last_match(2).downcase.to_sym - - chains << new(name: name, - policy: policy, - ensure: :present) - - debug "[instance] '#{name}' #{policy}" - elsif line =~ %r{^\*(\S+)} - table = Regexp.last_match(1) - else - next - end - end - rescue Puppet::Error - # ignore command not found for ebtables or anything that doesn't exist - end - end - - chains - end - - def allvalidchains - @resource[:name].match(NAME_FORMAT) - chain = Regexp.last_match(1) - table = Regexp.last_match(2) - protocol = Regexp.last_match(3) - yield MAPPING[protocol.to_sym][:tables], chain, table, protocol.to_sym - end -end diff --git a/lib/puppet/type/firewall.rb b/lib/puppet/type/firewall.rb index 2dfb3136b..145574189 100644 --- a/lib/puppet/type/firewall.rb +++ b/lib/puppet/type/firewall.rb @@ -1,229 +1,141 @@ # frozen_string_literal: true -# See: #10295 for more details. -# -# This is a workaround for bug: #4248 whereby ruby files outside of the normal -# provider/type path do not load until pluginsync has occured on the puppet server -# -# In this case I'm trying the relative path first, then falling back to normal -# mechanisms. This should be fixed in future versions of puppet but it looks -# like we'll need to maintain this for some time perhaps. -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..')) -require 'puppet/util/firewall' +# lib/puppet/type/iptables.rb +require 'puppet/resource_api' -Puppet::Type.newtype(:firewall) do - include Puppet::Util::Firewall +Puppet::ResourceApi.register_type( + name: 'firewall', + docs: <<-DESC, + This type provides the capability to manage firewall rules within puppet via iptables. - @doc = <<-PUPPETCODE - @summary - This type provides the capability to manage firewall rules within puppet. + **Autorequires:** - **Autorequires:** + If Puppet is managing the iptables chains specified in the + `chain` or `jump` parameters, the firewall resource will autorequire + those firewallchain resources. - If Puppet is managing the iptables or ip6tables chains specified in the - `chain` or `jump` parameters, the firewall resource will autorequire - those firewallchain resources. + If Puppet is managing the iptables, iptables-persistent, or iptables-services packages, + the firewall resource will autorequire those packages to ensure that any required binaries are + installed. - If Puppet is managing the iptables, iptables-persistent, or iptables-services packages, - and the provider is iptables or ip6tables, the firewall resource will - autorequire those packages to ensure that any required binaries are - installed. + #### Providers + * Required binaries: iptables-save, iptables. + * Default for kernel == linux. + * Supported features: address_type, clusterip, connection_limiting, conntrack, dnat, icmp_match, + interface_match, iprange, ipsec_dir, ipsec_policy, ipset, iptables, isfragment, length, + log_level, log_prefix, log_uid, log_tcp_sequence, log_tcp_options, log_ip_options, + mark, mask, mss, netmap, nflog_group, nflog_prefix, + nflog_range, nflog_threshold, owner, pkttype, queue_bypass, queue_num, rate_limiting, + recent_limiting, reject_type, snat, socket, state_match, string_matching, tcp_flags, bpf. - #### Providers - Note: Not all features are available with all providers. + #### Features + * address_type: The ability to match on source or destination address type. - * ip6tables: Ip6tables type provider + * clusterip: Configure a simple cluster of nodes that share a certain IP and MAC address without an explicit load balancer in front of them. - * Required binaries: ip6tables-save, ip6tables. - * Supported features: address_type, connection_limiting, conntrack, dnat, hop_limiting, icmp_match, - interface_match, iprange, ipsec_dir, ipsec_policy, ipset, iptables, isfirstfrag, - ishasmorefrags, islastfrag, length, log_level, log_prefix, log_uid, - log_tcp_sequence, log_tcp_options, log_ip_options, mask, mss, - owner, pkttype, queue_bypass, queue_num, rate_limiting, recent_limiting, reject_type, - snat, socket, state_match, string_matching, tcp_flags, hashlimit, bpf. + * condition: Match if a specific condition variable is (un)set (requires xtables-addons) - * iptables: Iptables type provider + * connection_limiting: Connection limiting features. - * Required binaries: iptables-save, iptables. - * Default for kernel == linux. - * Supported features: address_type, clusterip, connection_limiting, conntrack, dnat, icmp_match, - interface_match, iprange, ipsec_dir, ipsec_policy, ipset, iptables, isfragment, length, - log_level, log_prefix, log_uid, log_tcp_sequence, log_tcp_options, log_ip_options, - mark, mask, mss, netmap, nflog_group, nflog_prefix, - nflog_range, nflog_threshold, owner, pkttype, queue_bypass, queue_num, rate_limiting, - recent_limiting, reject_type, snat, socket, state_match, string_matching, tcp_flags, bpf. + * conntrack: Connection tracking features. - #### Features - * address_type: The ability to match on source or destination address type. + * dnat: Destination NATing. - * clusterip: Configure a simple cluster of nodes that share a certain IP and MAC address without an explicit load balancer in front of them. + * hop_limiting: Hop limiting features. - * condition: Match if a specific condition variable is (un)set (requires xtables-addons) + * icmp_match: The ability to match ICMP types. - * connection_limiting: Connection limiting features. + * interface_match: Interface matching. - * conntrack: Connection tracking features. + * iprange: The ability to match on source or destination IP range. - * dnat: Destination NATing. + * ipsec_dir: The ability to match IPsec policy direction. - * hop_limiting: Hop limiting features. + * ipsec_policy: The ability to match IPsec policy. - * icmp_match: The ability to match ICMP types. + * iptables: The provider provides iptables features. - * interface_match: Interface matching. + * isfirstfrag: The ability to match the first fragment of a fragmented ipv6 packet. - * iprange: The ability to match on source or destination IP range. + * isfragment: The ability to match fragments. - * ipsec_dir: The ability to match IPsec policy direction. + * ishasmorefrags: The ability to match a non-last fragment of a fragmented ipv6 packet. - * ipsec_policy: The ability to match IPsec policy. + * islastfrag: The ability to match the last fragment of an ipv6 packet. - * iptables: The provider provides iptables features. + * length: The ability to match the length of the layer-3 payload. - * isfirstfrag: The ability to match the first fragment of a fragmented ipv6 packet. + * log_level: The ability to control the log level. - * isfragment: The ability to match fragments. + * log_prefix: The ability to add prefixes to log messages. - * ishasmorefrags: The ability to match a non-last fragment of a fragmented ipv6 packet. + * log_uid: The ability to log the userid of the process which generated the packet. - * islastfrag: The ability to match the last fragment of an ipv6 packet. + * log_tcp_sequence: The ability to log TCP sequence numbers. - * length: The ability to match the length of the layer-3 payload. + * log_tcp_options: The ability to log TCP packet header. - * log_level: The ability to control the log level. + * log_ip_options: The ability to log IP/IPv6 packet header. - * log_prefix: The ability to add prefixes to log messages. + * mark: The ability to match or set the netfilter mark value associated with the packet. - * log_uid: The ability to log the userid of the process which generated the packet. + * mask: The ability to match recent rules based on the ipv4 mask. - * log_tcp_sequence: The ability to log TCP sequence numbers. + * nflog_group: The ability to set the group number for NFLOG. - * log_tcp_options: The ability to log TCP packet header. + * nflog_prefix: The ability to set a prefix for nflog messages. - * log_ip_options: The ability to log IP/IPv6 packet header. + * nflog_size: Set the max size of a message to send to nflog. - * mark: The ability to match or set the netfilter mark value associated with the packet. + * nflog_threshold: The ability to set nflog_threshold. - * mask: The ability to match recent rules based on the ipv4 mask. + * owner: The ability to match owners. - * nflog_group: The ability to set the group number for NFLOG. + * pkttype: The ability to match a packet type. - * nflog_prefix: The ability to set a prefix for nflog messages. + * rate_limiting: Rate limiting features. - * nflog_size: Set the max size of a message to send to nflog. + * recent_limiting: The netfilter recent module. - * nflog_threshold: The ability to set nflog_threshold. + * reject_type: The ability to control reject messages. - * owner: The ability to match owners. + * set_mss: Set the TCP MSS of a packet. - * pkttype: The ability to match a packet type. + * snat: Source NATing. - * rate_limiting: Rate limiting features. + * socket: The ability to match open sockets. - * recent_limiting: The netfilter recent module. + * state_match: The ability to match stateful firewall states. - * reject_type: The ability to control reject messages. + * string_matching: The ability to match a given string by using some pattern matching strategy. - * set_mss: Set the TCP MSS of a packet. + * tcp_flags: The ability to match on particular TCP flag settings. - * snat: Source NATing. + * netmap: The ability to map entire subnets via source or destination nat rules. - * socket: The ability to match open sockets. + * hashlimit: The ability to use the hashlimit-module. - * state_match: The ability to match stateful firewall states. + * bpf: The ability to use Berkeley Paket Filter rules. - * string_matching: The ability to match a given string by using some pattern matching strategy. + * ipvs: The ability to match IP Virtual Server packets. - * tcp_option: The ability to match on particular TCP options. + * ct_target: The ability to set connection tracking parameters for a packet or its associated connection. - * tcp_flags: The ability to match on particular TCP flag settings. - - * netmap: The ability to map entire subnets via source or destination nat rules. - - * hashlimit: The ability to use the hashlimit-module. - - * bpf: The ability to use Berkeley Paket Filter rules. - - * ipvs: The ability to match IP Virtual Server packets. - - * ct_target: The ability to set connection tracking parameters for a packet or its associated connection. - - * random_fully: The ability to use --random-fully flag. - PUPPETCODE - - feature :connection_limiting, 'Connection limiting features.' - feature :condition, 'Match if a specific condition variable is (un)set.' - feature :conntrack, 'Connection tracking features.' - feature :hop_limiting, 'Hop limiting features.' - feature :rate_limiting, 'Rate limiting features.' - feature :recent_limiting, 'The netfilter recent module' - feature :snat, 'Source NATing' - feature :dnat, 'Destination NATing' - feature :netmap, 'NET MAPping' - feature :interface_match, 'Interface matching' - feature :icmp_match, 'Matching ICMP types' - feature :owner, 'Matching owners' - feature :state_match, 'Matching stateful firewall states' - feature :reject_type, 'The ability to control reject messages' - feature :log_level, 'The ability to control the log level' - feature :log_prefix, 'The ability to add prefixes to log messages' - feature :log_uid, 'Add UIDs to log messages' - feature :log_tcp_sequence, 'Add TCP sequence numbers to log messages' - feature :log_tcp_options, 'Add TCP packet header to log messages' - feature :log_ip_options, 'Add IP/IPv6 packet header to log messages' - feature :mark, 'Match or Set the netfilter mark value associated with the packet' - feature :mss, 'Match a given TCP MSS value or range.' - feature :tcp_option, 'The ability to match on particular TCP options' - feature :tcp_flags, 'The ability to match on particular TCP flag settings' - feature :pkttype, 'Match a packet type' - feature :rpfilter, 'Perform reverse-path filtering' - feature :socket, 'Match open sockets' - feature :isfragment, 'Match fragments' - feature :address_type, 'The ability match on source or destination address type' - feature :iprange, 'The ability match on source or destination IP range ' - feature :ishasmorefrags, 'Match a non-last fragment of a fragmented ipv6 packet - might be first' - feature :islastfrag, 'Match the last fragment of an ipv6 packet' - feature :isfirstfrag, 'Match the first fragment of a fragmented ipv6 packet' - feature :ipsec_policy, 'Match IPsec policy' - feature :ipsec_dir, 'Match IPsec policy direction' - feature :mask, 'Ability to match recent rules based on the ipv4 mask' - feature :nflog_group, 'netlink group to subscribe to for logging' - feature :nflog_prefix, '' - feature :nflog_range, '' - feature :nflog_size, '' - feature :nflog_threshold, '' - feature :ipset, 'Match against specified ipset list' - feature :clusterip, 'Configure a simple cluster of nodes that share a certain IP and MAC address without an explicit load balancer in front of them.' - feature :length, 'Match the length of layer-3 payload' - feature :string_matching, 'String matching features' - feature :queue_num, 'Which NFQUEUE to send packets to' - feature :queue_bypass, 'If nothing is listening on queue_num, allow packets to bypass the queue' - feature :hashlimit, 'Hashlimit features' - feature :bpf, 'Berkeley Paket Filter feature' - feature :ipvs, 'Packet belongs to an IP Virtual Server connection' - feature :ct_target, 'The ability to set connection tracking parameters for a packet or its associated connection' - feature :random_fully, 'The ability to use --random-fully flag' - # provider specific features - feature :iptables, 'The provider provides iptables features.' - - ensurable do - desc <<-PUPPETCODE - Manage the state of this rule. - PUPPETCODE - - newvalue(:present) do - provider.insert - end - - newvalue(:absent) do - provider.delete - end - - defaultto :present - end - - newparam(:name) do - desc <<-PUPPETCODE + * random_fully: The ability to use --random-fully flag. + DESC + features: ['custom_insync'], + attributes: { + ensure: { + type: "Enum[present, absent, 'present', 'absent']", + default: 'present', + desc: <<-DESC + Whether this rule should be present or absent on the target system. + DESC + }, + name: { + type: 'Pattern[/(^\d+(?:[ \t-]\S+)+$)/]', + behaviour: :namevar, + desc: <<-DESC The canonical name of the rule. This name is also used for ordering so make sure you prefix the rule with a number: @@ -232,30 +144,61 @@ Depending on the provider, the name of the rule can be stored using the comment feature of the underlying firewall subsystem. - PUPPETCODE - isnamevar - - # Keep rule names simple - they must start with a number - newvalues(%r{^\d+[[:graph:][:space:]]+$}) - end - - newproperty(:action) do - desc <<-PUPPETCODE - This is the action to perform on a match. Can be one of: - - * accept - the packet is accepted - * reject - the packet is rejected with a suitable ICMP response - * drop - the packet is dropped - - If you specify no value it will simply match the rule but perform no - action unless you provide a provider specific parameter (such as *jump*). - PUPPETCODE - newvalues(:accept, :reject, :drop) - end - - # Generic matching properties - newproperty(:source) do - desc <<-PUPPETCODE + DESC + }, + line: { + type: 'Optional[String[1]]', + behaviour: :read_only, + desc: <<-DESC + A read only attribute containing the full rule, used when deleting and when applying firewallchain purge attributes. + DESC + }, + protocol: { + type: "Enum['iptables', 'ip6tables', 'IPv4', 'IPv6']", + default: 'IPv4', + desc: <<-DESC + The protocol used to set the rule, it's allowed values have been expanded to bring it closer to its `firewallchain` counterpart. + Defaults to `IPv4` + + Noted: this was previously defined as `provider`, however the resource_api does not allow this to be used as an attribute title. + DESC + }, + table: { + type: "Enum['nat', 'mangle', 'filter', 'raw', 'rawpost', 'broute', 'security']", + default: 'filter', + desc: <<-DESC + The table the rule will exist in. + Valid options are: + + * nat + * mangle + * filter + * raw + * rawpost + + Defaults to 'filter' + DESC + }, + chain: { + type: 'String[1]', + default: 'INPUT', + desc: <<-DESC + Name of the chain the rule will be a part of, ensure the chain you choose exists within your set table. + Can be one of the built-in chains: + + * INPUT + * FORWARD + * OUTPUT + * PREROUTING + * POSTROUTING + + Or you can provide a user-based chain. + Defaults to 'INPUT' + DESC + }, + source: { + type: 'Optional[String[1]]', + desc: <<-DESC The source address. For example: source => '192.168.2.0/24' @@ -265,54 +208,11 @@ source => '! 192.168.2.0/24' The source can also be an IPv6 address if your provider supports it. - PUPPETCODE - - munge do |value| - case @resource[:provider] - when :iptables - protocol = :IPv4 - when :ip6tables - protocol = :IPv6 - else - raise('cannot work out protocol family') - end - - begin - @resource.host_to_mask(value, protocol) - rescue StandardError => e - raise("host_to_ip failed for #{value}, exception #{e}") - end - end - end - - # Source IP range - newproperty(:src_range, required_features: :iprange) do - desc <<-PUPPETCODE - The source IP range. For example: - - src_range => '192.168.1.1-192.168.1.10' - - The source IP range must be in 'IP1-IP2' format. - PUPPETCODE - - validate do |value| - matches = %r{^([^\-\/]+)-([^\-\/]+)$}.match(value) - raise(ArgumentError, "The source IP range must be in 'IP1-IP2' format.") unless matches - start_addr = matches[1] - end_addr = matches[2] - - [start_addr, end_addr].each do |addr| - begin # rubocop:disable Style/RedundantBegin - @resource.host_to_ip(addr) - rescue StandardError - raise("Invalid IP address \"#{addr}\" in range \"#{value}\"") - end - end - end - end - - newproperty(:destination) do - desc <<-PUPPETCODE + DESC + }, + destination: { + type: 'Optional[String[1]]', + desc: <<-DESC The destination address to match. For example: destination => '192.168.1.0/24' @@ -322,164 +222,260 @@ destination => '! 192.168.2.0/24' The destination can also be an IPv6 address if your provider supports it. - PUPPETCODE - - munge do |value| - case @resource[:provider] - when :iptables - protocol = :IPv4 - when :ip6tables - protocol = :IPv6 - else - raise('cannot work out protocol family') - end - - begin - @resource.host_to_mask(value, protocol) - rescue StandardError => e - raise("host_to_ip failed for #{value}, exception #{e}") - end - end - end - - # Destination IP range - newproperty(:dst_range, required_features: :iprange) do - desc <<-PUPPETCODE - The destination IP range. For example: + DESC + }, + iniface: { + type: 'Optional[Pattern[/^(?:!\s)?[a-zA-Z0-9\-\._\+\:@]+$/]]', + desc: <<-DESC + Input interface to filter on. Supports interface alias like eth0:0. + To negate the match try this: - dst_range => '192.168.1.1-192.168.1.10' + iniface => '! lo', + DESC + }, + outiface: { + type: 'Optional[Pattern[/^(?:!\s)?[a-zA-Z0-9\-\._\+\:@]+$/]]', + desc: <<-DESC + Output interface to filter on. Supports interface alias like eth0:0. + To negate the match try this: - The destination IP range must be in 'IP1-IP2' format. - PUPPETCODE - - validate do |value| - matches = %r{^([^\-\/]+)-([^\-\/]+)$}.match(value) - raise(ArgumentError, "The destination IP range must be in 'IP1-IP2' format.") unless matches - start_addr = matches[1] - end_addr = matches[2] - - [start_addr, end_addr].each do |addr| - begin # rubocop:disable Style/RedundantBegin - @resource.host_to_ip(addr) - rescue StandardError - raise("Invalid IP address \"#{addr}\" in range \"#{value}\"") - end - end - end - end - - newproperty(:proto) do - desc <<-PUPPETCODE - The specific protocol to match for this rule. - PUPPETCODE + outiface => '! lo', + DESC + }, + physdev_in: { + type: 'Optional[Pattern[/^(?:!\s)?[a-zA-Z0-9\-\._\+]+$/]]', + desc: <<-DESC + Match if the packet is entering a bridge from the given interface. + To negate the match try this: - newvalues(*[:ip, :tcp, :udp, :icmp, :"ipv6-icmp", :esp, :ah, :vrrp, :carp, :igmp, :ipencap, :ipv4, :ipv6, :ospf, :gre, :cbt, :sctp, :pim, :all].map { |proto| - [proto, "! #{proto}".to_sym] - }.flatten) - defaultto 'tcp' - end + physdev_in => '! lo', + DESC + }, + physdev_out: { + type: 'Optional[Pattern[/^(?:!\s)?[a-zA-Z0-9\-\._\+]+$/]]', + desc: <<-DESC + Match if the packet is leaving a bridge via the given interface. + To negate the match try this: - newproperty(:sport, array_matching: :all) do - desc <<-PUPPETCODE - The source port to match for this filter (if the protocol supports - ports). Will accept a single element or an array. + physdev_out => '! lo', + DESC + }, + physdev_is_bridged: { + type: 'Optional[Boolean]', + desc: <<-DESC + Match if the packet is transversing a bridge. + DESC + }, + physdev_is_in: { + type: 'Optional[Boolean]', + desc: <<-DESC + Matches if the packet has entered through a bridge interface. + DESC + }, + physdev_is_out: { + type: 'Optional[Boolean]', + desc: <<-DESC + Matches if the packet will leave through a bridge interface. + DESC + }, + proto: { + type: 'Optional[Pattern[/^(?:!\s)?(?:ip(?:encap)?|tcp|udp|icmp|esp|ah|vrrp|carp|igmp|ipv4|ospf|gre|cbt|sctp|pim|all)/]]', + default: 'tcp', + desc: <<-DESC + The specific protocol to match for this rule. + DESC + }, + isfragment: { + type: 'Optional[Boolean]', + desc: <<-DESC + Set to true to match tcp fragments (requires proto to be set to tcp) + DESC + }, + isfirstfrag: { + type: 'Optional[Boolean]', + desc: <<-DESC + Matches if the packet is the first fragment. + Specific to IPv6. + DESC + }, + ishasmorefrags: { + type: 'Optional[Boolean]', + desc: <<-DESC + Matches if the packet has it's 'more fragments' bit set. + Specific to IPv6. + DESC + }, + islastfrag: { + type: 'Optional[Boolean]', + desc: <<-DESC + Matches if the packet is the last fragment. + Specific to IPv6. + DESC + }, + stat_mode: { + type: 'Optional[Enum[nth, random]]', + desc: <<-DESC + Set the matching mode for statistic matching. + DESC + }, + stat_every: { + type: 'Optional[Integer[1]]', + desc: <<-DESC + Match one packet every nth packet. Requires `stat_mode => 'nth'` + DESC + }, + stat_packet: { + type: 'Optional[Integer]', + desc: <<-DESC + Set the initial counter value for the nth mode. Must be between 0 and the value of `stat_every`. + Defaults to 0. Requires `stat_mode => 'nth'` + DESC + }, + stat_probability: { + type: 'Optional[Variant[Integer[0,1], Float[0.0,1.0]]]', + desc: <<-DESC + Set the probability from 0 to 1 for a packet to be randomly matched. It works only with `stat_mode => 'random'`. + DESC + }, + src_range: { + type: 'Optional[String[1]]', + desc: <<-DESC + The source IP range. For example: - For some firewall providers you can pass a range of ports in the format: + src_range => '192.168.1.1-192.168.1.10' - - + You can also negate the range by apending a `!`` to the front. For example: - For example: + src_range => '! 192.168.1.1-192.168.1.10' - 1-1024 + The source IP range must be in 'IP1-IP2' format. + DESC + }, + dst_range: { + type: 'Optional[String[1]]', + desc: <<-DESC + The destination IP range. For example: - This would cover ports 1 to 1024. - PUPPETCODE + dst_range => '192.168.1.1-192.168.1.10' - munge do |value| - @resource.string_to_port(value, @resource[:proto]) - end + You can also negate the range by putting ! in front. For example: - def to_s?(value) - should_to_s(value) - end + dst_range => '! 192.168.1.1-192.168.1.10' - def should_to_s(value) - value = [value] unless value.is_a?(Array) - value.join(',') - end - end + The destination IP range must be in 'IP1-IP2' format. + DESC + }, + tcp_option: { + type: 'Optional[Variant[Pattern[/^(?:!\s)?(?:[0-1][0-9]{0,2}|2[0-4][0-9]|25[0-5])$/], Integer[0,255]]]', + desc: <<-DESC + Match when the TCP option is present or absent. + Given as a single TCP option, optionally prefixed with '! ' to match + on absence instead. Only one TCP option can be matched in a given rule. + TCP option numbers are an eight-bit field, so valid option numbers range + from 0-255. + DESC + }, + tcp_flags: { + type: 'Optional[Pattern[/^(?:!\s)?((FIN|SYN|RST|PSH|ACK|URG|ALL|NONE),?)+\s((FIN|SYN|RST|PSH|ACK|URG|ALL|NONE),?)+$/]]', + desc: <<-DESC + Match when the TCP flags are as specified. + Is a string with a list of comma-separated flag names for the mask, + then a space, then a comma-separated list of flags that should be set. + The flags are: FIN SYN RST PSH ACK URG ALL NONE + Note that you specify them in the order that iptables --list-rules + would list them to avoid having puppet think you changed the flags. - newproperty(:dport, array_matching: :all) do - desc <<-PUPPETCODE - The destination port to match for this filter (if the protocol supports + Example: FIN,SYN,RST,ACK SYN matches packets with the SYN bit set and the + ACK,RST and FIN bits cleared. Such packets are used to request + TCP connection initiation. + Can be negated by placing ! in front, i.e. + ! FIN,SYN,RST,ACK SYN + DESC + }, + uid: { + type: 'Optional[Variant[String[1], Integer]]', + desc: <<-DESC + UID or Username owner matching rule. Accepts a single argument + only, as iptables does not accept multiple uid in a single + statement. + To negate add a space seperated '!' in front of the value. + DESC + }, + gid: { + type: 'Optional[Variant[String[1], Integer]]', + desc: <<-DESC + GID or Group owner matching rule. Accepts a single argument + only, as iptables does not accept multiple gid in a single + statement. + To negate add a space seperated '!' in front of the value. + DESC + }, + mac_source: { + type: 'Optional[Pattern[/^(?:!\s)?([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$/]]', + desc: <<-DESC + MAC Source + DESC + }, + sport: { + type: 'Optional[Variant[Array[Variant[Pattern[/^(?:!\s)?\d+(?:(?:\:|-)\d+)?$/],Integer]],Pattern[/^(?:!\s)?\d+(?:(?:\:|-)\d+)?$/],Integer]]', + desc: <<-DESC + The source port to match for this filter (if the protocol supports ports). Will accept a single element or an array. For some firewall providers you can pass a range of ports in the format: - - - - For example: - - 1-1024 + sport => '1:1024' This would cover ports 1 to 1024. - PUPPETCODE - munge do |value| - @resource.string_to_port(value, @resource[:proto]) - end + You can also negate a port by putting ! in front. For example: - def to_s?(value) - should_to_s(value) - end + sport => '! 54' - def should_to_s(value) - value = [value] unless value.is_a?(Array) - value.join(',') - end - end + If you wish to negate multiple ports at once, then place a ! at the start of the first array + variable. For example: - newproperty(:port, array_matching: :all) do - desc <<-PUPPETCODE - *note* This property has been DEPRECATED + sport => ['! 54','23'] - The destination or source port to match for this filter (if the protocol - supports ports). Will accept a single element or an array. + Note: + This will negate all passed ports, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. + DESC + }, + dport: { + type: 'Optional[Variant[Array[Variant[Pattern[/^(?:!\s)?\d+(?:(?:\:|-)\d+)?$/],Integer]],Pattern[/^(?:!\s)?\d+(?:(?:\:|-)\d+)?$/],Integer]]', + desc: <<-DESC + The source port to match for this filter (if the protocol supports + ports). Will accept a single element or an array. For some firewall providers you can pass a range of ports in the format: - - - - For example: - - 1-1024 + dport => '1:1024' This would cover ports 1 to 1024. - PUPPETCODE - validate do |_value| - Puppet.warning('Passing port to firewall is deprecated and will be removed. Use dport and/or sport instead.') - end + You can also negate a port by putting ! in front. For example: - munge do |value| - @resource.string_to_port(value, @resource[:proto]) - end + dport => '! 54' - def to_s?(value) - should_to_s(value) - end + If you wish to negate multiple ports at once, then place a ! at the start of the first array + variable. For example: - def should_to_s(value) - value = [value] unless value.is_a?(Array) - value.join(',') - end - end + dport => ['! 54','23'] - newproperty(:dst_type, required_features: :address_type, array_matching: :all) do - desc <<-PUPPETCODE - The destination address type. For example: + Note: + This will negate all passed ports, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. + DESC + }, + src_type: { + type: 'Optional[Variant[ + Array[Pattern[/^(?:!\s)?(?:UNSPEC|UNICAST|LOCAL|BROADCAST|ANYCAST|MULTICAST|BLACKHOLE|UNREACHABLE|UNREACHABLE|PROHIBIT|THROW|NAT|XRESOLVE)(?:\s--limit-iface-(?:in|out))?$/]], + Pattern[/^(?:!\s)?(?:UNSPEC|UNICAST|LOCAL|BROADCAST|ANYCAST|MULTICAST|BLACKHOLE|UNREACHABLE|UNREACHABLE|PROHIBIT|THROW|NAT|XRESOLVE)(?:\s--limit-iface-(?:in|out))?$/]]]', + desc: <<-DESC + The source address type. For example: - dst_type => ['LOCAL'] + src_type => 'LOCAL' Can be one of: @@ -498,33 +494,23 @@ def should_to_s(value) In addition, it accepts '--limit-iface-in' and '--limit-iface-out' flags, specified as: - dst_type => ['LOCAL --limit-iface-in'] + src_type => ['LOCAL --limit-iface-in'] It can also be negated using '!': - dst_type => ['! LOCAL'] + src_type => ['! LOCAL'] - Will accept a single element or an array. - PUPPETCODE - - newvalues(*[:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, - :BLACKHOLE, :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].map { |address_type| - [ - address_type, - "! #{address_type}".to_sym, - "#{address_type} --limit-iface-in".to_sym, - "#{address_type} --limit-iface-out".to_sym, - "! #{address_type} --limit-iface-in".to_sym, - "! #{address_type} --limit-iface-out".to_sym, - ] - }.flatten) - end - - newproperty(:src_type, required_features: :address_type, array_matching: :all) do - desc <<-PUPPETCODE - The source address type. For example: + Will accept a single element or an array. Each element of the array should be negated seperately. + DESC + }, + dst_type: { + type: 'Optional[Variant[ + Array[Pattern[/^(?:!\s)?(?:UNSPEC|UNICAST|LOCAL|BROADCAST|ANYCAST|MULTICAST|BLACKHOLE|UNREACHABLE|UNREACHABLE|PROHIBIT|THROW|NAT|XRESOLVE)(?:\s--limit-iface-(?:in|out))?$/]], + Pattern[/^(?:!\s)?(?:UNSPEC|UNICAST|LOCAL|BROADCAST|ANYCAST|MULTICAST|BLACKHOLE|UNREACHABLE|UNREACHABLE|PROHIBIT|THROW|NAT|XRESOLVE)(?:\s--limit-iface-(?:in|out))?$/]]]', + desc: <<-DESC + The destination address type. For example: - src_type => ['LOCAL'] + dst_type => ['LOCAL'] Can be one of: @@ -543,443 +529,43 @@ def should_to_s(value) In addition, it accepts '--limit-iface-in' and '--limit-iface-out' flags, specified as: - src_type => ['LOCAL --limit-iface-in'] + dst_type => ['LOCAL --limit-iface-in'] - It can also be negated using '!': + Each value can be negated seperately using '!': - src_type => ['! LOCAL'] + dst_type => ['! UNICAST', '! LOCAL'] Will accept a single element or an array. - PUPPETCODE - - newvalues(*[:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, - :BLACKHOLE, :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].map { |address_type| - [ - address_type, - "! #{address_type}".to_sym, - "#{address_type} --limit-iface-in".to_sym, - "#{address_type} --limit-iface-out".to_sym, - "! #{address_type} --limit-iface-in".to_sym, - "! #{address_type} --limit-iface-out".to_sym, - ] - }.flatten) - end - - # tcp-specific - newproperty(:mss) do - desc <<-PUPPETCODE - Match a given TCP MSS value or range. - PUPPETCODE - end - - # tcp-specific - newproperty(:tcp_option, required_features: :tcp_option) do - desc <<-PUPPETCODE - Match when the TCP option is present or absent. - Given as a single TCP option, optionally prefixed with '! ' to match - on absence instead. Only one TCP option can be matched in a given rule. - TCP option numbers are an eight-bit field, so valid option numbers range - from 0-255. - PUPPETCODE - - validate do |value| - unless value.to_i.bit_length < 8 && value.to_i >= 0 - raise ArgumentError, "TCP Options fall in the range 0-255, #{value} is not a valid TCP Option number" - end - end - munge { |value| value.to_s } - end - - # tcp-specific - newproperty(:tcp_flags, required_features: :tcp_flags) do - desc <<-PUPPETCODE - Match when the TCP flags are as specified. - Is a string with a list of comma-separated flag names for the mask, - then a space, then a comma-separated list of flags that should be set. - The flags are: SYN ACK FIN RST URG PSH ALL NONE - Note that you specify them in the order that iptables --list-rules - would list them to avoid having puppet think you changed the flags. - Example: FIN,SYN,RST,ACK SYN matches packets with the SYN bit set and the - ACK,RST and FIN bits cleared. Such packets are used to request - TCP connection initiation. - PUPPETCODE - end - - # Iptables specific - newproperty(:chain, required_features: :iptables) do - desc <<-PUPPETCODE - Name of the chain to use. Can be one of the built-ins: - - * INPUT - * FORWARD - * OUTPUT - * PREROUTING - * POSTROUTING - - Or you can provide a user-based chain. - PUPPETCODE - - defaultto 'INPUT' - newvalue(%r{^[a-zA-Z0-9\-_]+$}) - end - - newproperty(:table, required_features: :iptables) do - desc <<-PUPPETCODE - Table to use. Can be one of: - - * nat - * mangle - * filter - * raw - * rawpost - PUPPETCODE - - newvalues(:nat, :mangle, :filter, :raw, :rawpost) - defaultto 'filter' - end - - newproperty(:jump, required_features: :iptables) do - desc <<-PUPPETCODE - The value for the iptables --jump parameter. Normal values are: - - * QUEUE - * RETURN - * DNAT - * SNAT - * LOG - * NFLOG - * MASQUERADE - * REDIRECT - * MARK - * CT - - But any valid chain name is allowed. - - For the values ACCEPT, DROP, and REJECT, you must use the generic - 'action' parameter. This is to enfore the use of generic parameters where - possible for maximum cross-platform modelling. - - If you set both 'accept' and 'jump' parameters, you will get an error as - only one of the options should be set. - PUPPETCODE - - validate do |value| - unless %r{^[a-zA-Z0-9\-_]+$}.match?(value) - raise ArgumentError, <<-PUPPETCODE - Jump destination must consist of alphanumeric characters, an - underscore or a hyphen. - PUPPETCODE - end - - if ['accept', 'reject', 'drop'].include?(value.downcase) - raise ArgumentError, <<-PUPPETCODE - Jump destination should not be one of ACCEPT, REJECT or DROP. Use - the action property instead. - PUPPETCODE - end - end - end - - newproperty(:goto, required_features: :iptables) do - desc <<-PUPPETCODE - The value for the iptables --goto parameter. Normal values are: - - * QUEUE - * RETURN - * DNAT - * SNAT - * LOG - * MASQUERADE - * REDIRECT - * MARK - - But any valid chain name is allowed. - PUPPETCODE - - validate do |value| - unless %r{^[a-zA-Z0-9\-_]+$}.match?(value) - raise ArgumentError, <<-PUPPETCODE - Goto destination must consist of alphanumeric characters, an - underscore or a hyphen. - PUPPETCODE - end - - if ['accept', 'reject', 'drop'].include?(value.downcase) - raise ArgumentError, <<-PUPPETCODE - Goto destination should not be one of ACCEPT, REJECT or DROP. Use - the action property instead. - PUPPETCODE - end - end - end - - # Interface specific matching properties - newproperty(:iniface, required_features: :interface_match) do - desc <<-PUPPETCODE - Input interface to filter on. Supports interface alias like eth0:0. - To negate the match try this: - - iniface => '! lo', - - PUPPETCODE - newvalues(%r{^!?\s?[a-zA-Z0-9\-\._\+\:@]+$}) - end - - newproperty(:outiface, required_features: :interface_match) do - desc <<-PUPPETCODE - Output interface to filter on. Supports interface alias like eth0:0. - To negate the match try this: - - outiface => '! lo', - - PUPPETCODE - newvalues(%r{^!?\s?[a-zA-Z0-9\-\._\+\:@]+$}) - end - - # NAT specific properties - newproperty(:tosource, required_features: :snat) do - desc <<-PUPPETCODE - When using jump => "SNAT" you can specify the new source address using - this parameter. - PUPPETCODE - end - - newproperty(:todest, required_features: :dnat) do - desc <<-PUPPETCODE - When using jump => "DNAT" you can specify the new destination address - using this paramter. - PUPPETCODE - end - - newproperty(:toports, required_features: :dnat) do - desc <<-PUPPETCODE - For DNAT this is the port that will replace the destination port. - PUPPETCODE - end - - newproperty(:to, required_features: :netmap) do - desc <<-PUPPETCODE - For NETMAP this will replace the destination IP - PUPPETCODE - end - - newproperty(:random_fully, required_features: :random_fully) do - desc <<-PUPPETCODE - When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" - this boolean will enable fully randomized port mapping. - - **NOTE** Requires Kernel >= 3.13 and iptables >= 1.6.2 - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:random, required_features: :dnat) do - desc <<-PUPPETCODE - When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" - this boolean will enable randomized port mapping. - PUPPETCODE - - newvalues(:true, :false) - end - - # Reject ICMP type - newproperty(:reject, required_features: :reject_type) do - desc <<-PUPPETCODE - When combined with action => "REJECT" you can specify a different icmp - response to be sent back to the packet sender. - PUPPETCODE - end - - # Logging properties - newproperty(:log_level, required_features: :log_level) do - desc <<-PUPPETCODE - When combined with jump => "LOG" specifies the system log level to log - to. - PUPPETCODE - - munge do |value| - if value.is_a?(String) - value = @resource.log_level_name_to_number(value) - else - value - end - - if value.nil? && value != '' - raise('Unable to determine log level') - end - value - end - end - - newproperty(:log_prefix, required_features: :log_prefix) do - desc <<-PUPPETCODE - When combined with jump => "LOG" specifies the log prefix to use when - logging. - PUPPETCODE - - munge do |value| - if value == '' - raise('log_prefix should not be an empty string') - end - value - end - end - - newproperty(:log_uid, required_features: :log_uid) do - desc <<-PUPPETCODE - When combined with jump => "LOG" specifies the uid of the process making - the connection. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:log_tcp_sequence, required_features: :log_tcp_sequence) do - desc <<-PUPPETCODE - When combined with jump => "LOG" enables logging of the TCP sequence - numbers. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:log_tcp_options, required_features: :log_tcp_options) do - desc <<-PUPPETCODE - When combined with jump => "LOG" logging of the TCP packet - header. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:log_ip_options, required_features: :log_ip_options) do - desc <<-PUPPETCODE - When combined with jump => "LOG" logging of the TCP IP/IPv6 - packet header. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:nflog_group, required_features: :nflog_group) do - desc <<-PUPPETCODE - Used with the jump target NFLOG. - The netlink group (0 - 2^16-1) to which packets are (only applicable - for nfnetlink_log). Defaults to 0. - PUPPETCODE - - validate do |value| - if value.to_i > (2**16) - 1 || value.to_i < 0 - raise ArgumentError, 'nflog_group must be between 0 and 2^16-1' - end - end - - munge do |value| - if value.is_a?(String) && value =~ %r{^[-0-9]+$} - Integer(value) - else - value - end - end - end - - newproperty(:nflog_prefix, required_features: :nflog_prefix) do - desc <<-PUPPETCODE - Used with the jump target NFLOG. - A prefix string to include in the log message, up to 64 characters long, - useful for distinguishing messages in the logs. - PUPPETCODE - - validate do |value| - if value.length > 64 - raise ArgumentError, 'nflog_prefix must be less than 64 characters.' - end - end - end - - newproperty(:nflog_range, required_features: :nflog_range) do - desc <<-PUPPETCODE - Used with the jump target NFLOG. - This has never worked, use nflog_size instead. - PUPPETCODE - end - - newproperty(:nflog_size, required_features: :nflog_size) do - desc <<-PUPPETCODE - Used with the jump target NFLOG. - The number of bytes to be copied to userspace (only applicable for nfnetlink_log). - nfnetlink_log instances may specify their own size, this option overrides it. - PUPPETCODE - end - - newproperty(:nflog_threshold, required_features: :nflog_threshold) do - desc <<-PUPPETCODE - Used with the jump target NFLOG. - Number of packets to queue inside the kernel before sending them to userspace - (only applicable for nfnetlink_log). Higher values result in less overhead - per packet, but increase delay until the packets reach userspace. Defaults to 1. - PUPPETCODE - - munge do |value| - if value.is_a?(String) && value =~ %r{^[-0-9]+$} - Integer(value) - else - value - end - end - end - - # ICMP matching property - newproperty(:icmp, required_features: :icmp_match) do - desc <<-PUPPETCODE - When matching ICMP packets, this is the type of ICMP packet to match. - - A value of "any" is not supported. To achieve this behaviour the - parameter should simply be omitted or undefined. - An array of values is also not supported. To match against multiple ICMP - types, please use separate rules for each ICMP type. - PUPPETCODE - - validate do |value| - if value == 'any' - raise ArgumentError, - "Value 'any' is not valid. This behaviour should be achieved " \ - 'by omitting or undefining the ICMP parameter.' - end - if value.is_a?(Array) - raise ArgumentError, - 'Argument must not be an array of values. To match multiple ' \ - 'ICMP types, please use separate rules for each ICMP type.' - end - end - - munge do |value| - if value.is_a?(String) - # ICMP codes differ between IPv4 and IPv6. - case @resource[:provider] - when :iptables - protocol = 'inet' - when :ip6tables - protocol = 'inet6' - else - raise('cannot work out protocol family') - end - - value = @resource.icmp_name_to_number(value, protocol) - else - value - end - - if value.nil? && value != '' - raise('cannot work out icmp type') - end - value - end - end - - newproperty(:state, array_matching: :all, required_features: :state_match) do - desc <<-PUPPETCODE + DESC + }, + socket: { + type: 'Optional[Boolean]', + desc: <<-DESC + If true, matches if an open socket can be found by doing a coket lookup + on the packet. + DESC + }, + pkttype: { + type: "Optional[Enum['unicast', 'broadcast', 'multicast']]", + desc: <<-DESC + Sets the packet type to match. + DESC + }, + ipsec_dir: { + type: "Optional[Enum['in', 'out']]", + desc: <<-DESC + Sets the ipsec policy direction + DESC + }, + ipsec_policy: { + type: "Optional[Enum['none', 'ipsec']]", + desc: <<-DESC + Sets the ipsec policy type. May take a combination of arguments for any flags that can be passed to `--pol ipsec` such as: `--strict`, `--reqid 100`, `--next`, `--proto esp`, etc. + DESC + }, + state: { + type: 'Optional[Variant[Pattern[/^(?:!\s)?(?:INVALID|ESTABLISHED|NEW|RELATED|UNTRACKED)$/], Array[Pattern[/^(?:!\s)?(?:INVALID|ESTABLISHED|NEW|RELATED|UNTRACKED)$/]]]]', + desc: <<-DESC Matches a packet based on its state in the firewall stateful inspection table. Values can be: @@ -988,28 +574,28 @@ def should_to_s(value) * NEW * RELATED * UNTRACKED - PUPPETCODE + * SNAT + * DNAT - newvalues(:INVALID, :ESTABLISHED, :NEW, :RELATED, :UNTRACKED) + Can be passed either as a single String or as an Array: - # States should always be sorted. This normalizes the resource states to - # keep it consistent with the sorted result from iptables-save. - def should=(values) - @should = super(values).sort_by { |sym| sym.to_s } - end + state => 'INVALID' + state => ['INVALID', 'ESTABLISHED'] - def to_s?(value) - should_to_s(value) - end + Values can be negated by adding a '!'. + If you wish to negate multiple states at once, then place a ! at the start of the first array + variable. For example: - def should_to_s(value) - value = [value] unless value.is_a?(Array) - value.join(',') - end - end + state => ['! INVALID', 'ESTABLISHED'] - newproperty(:ctstate, array_matching: :all, required_features: :conntrack) do - desc <<-PUPPETCODE + Note: + This will negate all passed states, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. + DESC + }, + ctstate: { + type: 'Optional[Variant[Pattern[/^(?:!\s)?(?:INVALID|ESTABLISHED|NEW|RELATED|UNTRACKED|SNAT|DNAT)$/], Array[Pattern[/^(?:!\s)?(?:INVALID|ESTABLISHED|NEW|RELATED|UNTRACKED|SNAT|DNAT)$/]]]]', + desc: <<-DESC Matches a packet based on its state in the firewall stateful inspection table, using the conntrack module. Values can be: @@ -1020,36 +606,33 @@ def should_to_s(value) * UNTRACKED * SNAT * DNAT - PUPPETCODE - newvalues(:INVALID, :ESTABLISHED, :NEW, :RELATED, :UNTRACKED, :SNAT, :DNAT) + Can be passed either as a single String or as an Array, if passed as an array values should be passed in order: - # States should always be sorted. This normalizes the resource states to - # keep it consistent with the sorted result from iptables-save. - def should=(values) - @should = super(values).sort_by { |sym| sym.to_s } - end + ctstate => 'INVALID' + ctstate => ['INVALID', 'ESTABLISHED'] - def to_s?(value) - should_to_s(value) - end + Values can be negated by adding a '!'. + If you wish to negate multiple states at once, then place a ! at the start of the first array + variable. For example: - def should_to_s(value) - value = [value] unless value.is_a?(Array) - value.join(',') - end - end + ctstate => ['! INVALID', 'ESTABLISHED'] - newproperty(:ctproto, required_features: :conntrack) do - desc <<-PUPPETCODE + Note: + This will negate all passed states, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. + DESC + }, + ctproto: { + type: 'Optional[Variant[Pattern[/^(?:!\s)?\d+$/],Integer]]', + desc: <<-DESC The specific layer-4 protocol number to match for this rule using the conntrack module. - PUPPETCODE - newvalue(%r{^!?\s?\d+$}) - end - - newproperty(:ctorigsrc, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctorigsrc: { + type: 'Optional[String[1]]', + desc: <<-DESC The original source address using the conntrack module. For example: ctorigsrc => '192.168.2.0/24' @@ -1059,33 +642,11 @@ def should_to_s(value) ctorigsrc => '! 192.168.2.0/24' The ctorigsrc can also be an IPv6 address if your provider supports it. - PUPPETCODE - - munge do |value| - case @resource[:provider] - when :iptables - protocol = :IPv4 - when :ip6tables - protocol = :IPv6 - else - raise('cannot work out protocol family') - end - - begin - @resource.host_to_mask(value, protocol) - if protocol == :IPv4 - value.chomp('/32') - elsif protocol == :IPv6 - value.chomp('/128') - end - rescue StandardError => e - raise("host_to_ip failed for #{value}, exception #{e}") - end - end - end - - newproperty(:ctorigdst, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctorigdst: { + type: 'Optional[String[1]]', + desc: <<-DESC The original destination address using the conntrack module. For example: ctorigdst => '192.168.2.0/24' @@ -1095,33 +656,11 @@ def should_to_s(value) ctorigdst => '! 192.168.2.0/24' The ctorigdst can also be an IPv6 address if your provider supports it. - PUPPETCODE - - munge do |value| - case @resource[:provider] - when :iptables - protocol = :IPv4 - when :ip6tables - protocol = :IPv6 - else - raise('cannot work out protocol family') - end - - begin - @resource.host_to_mask(value, protocol) - if protocol == :IPv4 - value.chomp('/32') - elsif protocol == :IPv6 - value.chomp('/128') - end - rescue StandardError => e - raise("host_to_ip failed for #{value}, exception #{e}") - end - end - end - - newproperty(:ctreplsrc, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctreplsrc: { + type: 'Optional[String[1]]', + desc: <<-DESC The reply source address using the conntrack module. For example: ctreplsrc => '192.168.2.0/24' @@ -1131,33 +670,11 @@ def should_to_s(value) ctreplsrc => '! 192.168.2.0/24' The ctreplsrc can also be an IPv6 address if your provider supports it. - PUPPETCODE - - munge do |value| - case @resource[:provider] - when :iptables - protocol = :IPv4 - when :ip6tables - protocol = :IPv6 - else - raise('cannot work out protocol family') - end - - begin - @resource.host_to_mask(value, protocol) - if protocol == :IPv4 - value.chomp('/32') - elsif protocol == :IPv6 - value.chomp('/128') - end - rescue StandardError => e - raise("host_to_ip failed for #{value}, exception #{e}") - end - end - end - - newproperty(:ctrepldst, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctrepldst: { + type: 'Optional[String[1]]', + desc: <<-DESC The reply destination address using the conntrack module. For example: ctrepldst => '192.168.2.0/24' @@ -1167,33 +684,11 @@ def should_to_s(value) ctrepldst => '! 192.168.2.0/24' The ctrepldst can also be an IPv6 address if your provider supports it. - PUPPETCODE - - munge do |value| - case @resource[:provider] - when :iptables - protocol = :IPv4 - when :ip6tables - protocol = :IPv6 - else - raise('cannot work out protocol family') - end - - begin - @resource.host_to_mask(value, protocol) - if protocol == :IPv4 - value.chomp('/32') - elsif protocol == :IPv6 - value.chomp('/128') - end - rescue StandardError => e - raise("host_to_ip failed for #{value}, exception #{e}") - end - end - end - - newproperty(:ctorigsrcport, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctorigsrcport: { + type: 'Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]', + desc: <<-DESC The original source port to match for this filter using the conntrack module. For example: @@ -1206,13 +701,11 @@ def should_to_s(value) You can also negate a port by putting ! in front. For example: ctorigsrcport => '! 80' - - PUPPETCODE - newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) - end - - newproperty(:ctorigdstport, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctorigdstport: { + type: 'Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]', + desc: <<-DESC The original destination port to match for this filter using the conntrack module. For example: @@ -1225,13 +718,11 @@ def should_to_s(value) You can also negate a port by putting ! in front. For example: ctorigdstport => '! 80' - - PUPPETCODE - newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) - end - - newproperty(:ctreplsrcport, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctreplsrcport: { + type: 'Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]', + desc: <<-DESC The reply source port to match for this filter using the conntrack module. For example: @@ -1244,13 +735,11 @@ def should_to_s(value) You can also negate a port by putting ! in front. For example: ctreplsrcport => '! 80' - - PUPPETCODE - newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) - end - - newproperty(:ctrepldstport, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctrepldstport: { + type: 'Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]', + desc: <<-DESC The reply destination port to match for this filter using the conntrack module. For example: @@ -1263,355 +752,101 @@ def should_to_s(value) You can also negate a port by putting ! in front. For example: ctrepldstport => '! 80' - - PUPPETCODE - newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) - end - - newproperty(:ctstatus, array_matching: :all, required_features: :conntrack) do - desc <<-PUPPETCODE + DESC + }, + ctstatus: { + type: 'Optional[Variant[Pattern[/^(?:!\s)?(?:EXPECTED|SEEN_REPLY|ASSURED|CONFIRMED|NONE)$/], Array[Pattern[/^(?:!\s)?(?:EXPECTED|SEEN_REPLY|ASSURED|CONFIRMED|NONE)$/]]]]', + desc: <<-DESC Matches a packet based on its status using the conntrack module. Values can be: * EXPECTED * SEEN_REPLY * ASSURED * CONFIRMED - PUPPETCODE + * NONE - newvalues(:NONE, :EXPECTED, :SEEN_REPLY, :ASSURED, :CONFIRMED) + Can be passed either as a single String or as an Array: - # Statuses should always be sorted. This normalizes the resource status to - # keep it consistent with the sorted result from iptables-save. - def should=(values) - @should = super(values).sort_by { |sym| sym.to_s } - end + ctstatus => 'EXPECTED' + ctstatus => ['EXPECTED', 'CONFIRMED'] - def to_s?(value) - should_to_s(value) - end + Values can be negated by adding a '!'. + If you wish to negate multiple states at once, then place a ! at the start of the first array + variable. For example: - def should_to_s(value) - value = [value] unless value.is_a?(Array) - value.join(',') - end - end + ctstatus => ['! EXPECTED', 'CONFIRMED'] - newproperty(:ctexpire, required_features: :conntrack) do - desc <<-PUPPETCODE - Matches a packet based on lifetime remaining in seconds or range of values + Note:#{' '} + This will negate all passed states, it is not possible to negate a single one of the array. + In order to maintain compatibility it is also possible to negate all values given in the array to achieve the same behaviour. + DESC + }, + ctexpire: { + type: 'Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]', + desc: <<-DESC + Matches a packet based on lifetime remaining in seconds or range of seconds using the conntrack module. For example: - ctexpire => '100:150' - - PUPPETCODE - newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) - end - - newproperty(:ctdir, required_features: :conntrack) do - desc <<-PUPPETCODE + ctexpire => '100' + ctexpire => '100:150' + DESC + }, + ctdir: { + type: "Optional[Enum['REPLY', 'ORIGINAL']]", + desc: <<-DESC Matches a packet that is flowing in the specified direction using the conntrack module. If this flag is not specified at all, matches packets in both directions. Values can be: * REPLY * ORIGINAL - PUPPETCODE - - newvalues(:REPLY, :ORIGINAL) - end - - # Connection mark - newproperty(:connmark, required_features: :mark) do - desc <<-PUPPETCODE - Match the Netfilter mark value associated with the packet. Accepts either of: - mark/mask or mark. These will be converted to hex if they are not already. - PUPPETCODE - munge do |value| - int_or_hex = '[a-fA-F0-9x]' - match = value.to_s.match("(#{int_or_hex}+)(/)?(#{int_or_hex}+)?") - mark = @resource.to_hex32(match[1]) - - # Values that can't be converted to hex. - # Or contain a trailing slash with no mask. - if mark.nil? || (mark && match[2] && match[3].nil?) - raise ArgumentError, 'MARK value must be integer or hex between 0 and 0xffffffff' - end - - # There should not be a mask on connmark - unless match[3].nil? - raise ArgumentError, 'iptables does not support masks on MARK match rules' - end - value = mark - - value - end - end - - # Connection limiting properties - newproperty(:connlimit_above, required_features: :connection_limiting) do - desc <<-PUPPETCODE - Connection limiting value for matched connections above n. - PUPPETCODE - newvalue(%r{^\d+$}) - end - - newproperty(:connlimit_mask, required_features: :connection_limiting) do - desc <<-PUPPETCODE - Connection limiting by subnet mask for matched connections. - IPv4: 0-32 - IPv6: 0-128 - PUPPETCODE - newvalue(%r{^\d+$}) - end - - # Hop limiting properties - newproperty(:hop_limit, required_features: :hop_limiting) do - desc <<-PUPPETCODE + DESC + }, + hop_limit: { + type: 'Optional[Variant[Pattern[/^(?:!\s)?\d+$/],Integer]]', + desc: <<-DESC Hop limiting value for matched packets. - PUPPETCODE - newvalue(%r{^\d+$}) - end + To negate add a space seperated `!` the the beginning of the value + This is IPv6 specific. + DESC + }, + icmp: { + type: 'Optional[Variant[String[1],Integer]]', + desc: <<-DESC + When matching ICMP packets, this is the type of ICMP packet to match. - # Rate limiting properties - newproperty(:limit, required_features: :rate_limiting) do - desc <<-PUPPETCODE + A value of "any" is not supported. To achieve this behaviour the + parameter should simply be omitted or undefined. + An array of values is also not supported. To match against multiple ICMP + types, please use separate rules for each ICMP type. + DESC + }, + limit: { + type: 'Optional[Pattern[/^\d+\/(?:sec(?:ond)?|min(?:ute)?|hour|day)$/]]', + desc: <<-DESC Rate limiting value for matched packets. The format is: - rate/[/second/|/minute|/hour|/day]. + rate/[/second/|/minute|/hour|/day] Example values are: '50/sec', '40/min', '30/hour', '10/day'." - PUPPETCODE - end - - newproperty(:burst, required_features: :rate_limiting) do - desc <<-PUPPETCODE + DESC + }, + burst: { + type: 'Optional[Integer[1]]', + desc: <<-DESC Rate limiting burst value (per second) before limit checks apply. - PUPPETCODE - newvalue(%r{^\d+$}) - end - - newproperty(:uid, required_features: :owner) do - desc <<-PUPPETCODE - UID or Username owner matching rule. Accepts a string argument - only, as iptables does not accept multiple uid in a single - statement. - PUPPETCODE - def insync?(is) - return false unless is - - require 'etc' - - # The following code allow us to take into consideration unix mappings - # between string usernames and UIDs (integers). We also need to ignore - # spaces as they are irrelevant with respect to rule sync. - - # Remove whitespace - is = is.gsub(%r{\s+}, '') - should = @should.first.to_s.gsub(%r{\s+}, '') - - # Keep track of negation, but remove the '!' - is_negate = '' - should_negate = '' - if is.start_with?('!') - is = is.gsub(%r{^!}, '') - is_negate = '!' - end - if should.start_with?('!') - should = should.gsub(%r{^!}, '') - should_negate = '!' - end - - # If 'should' contains anything other than digits or digit range, - # we assume that we have to do a lookup to convert - # to UID - unless should[%r{[0-9]+(-[0-9]+)?}] == should - should = Etc.getpwnam(should).uid - end - - # If 'is' contains anything other than digits or digit range, - # we assume that we have to do a lookup to convert - # to UID - unless is[%r{[0-9]+(-[0-9]+)?}] == is - is = Etc.getpwnam(is).uid - end - - "#{is_negate}#{is}" == "#{should_negate}#{should}" - end - end - - newproperty(:gid, required_features: :owner) do - desc <<-PUPPETCODE - GID or Group owner matching rule. Accepts a string argument - only, as iptables does not accept multiple gid in a single - statement. - PUPPETCODE - def insync?(is) - return false unless is - - require 'etc' - - # The following code allow us to take into consideration unix mappings - # between string group names and GIDs (integers). We also need to ignore - # spaces as they are irrelevant with respect to rule sync. - - # Remove whitespace - is = is.gsub(%r{\s+}, '') - should = @should.first.to_s.gsub(%r{\s+}, '') - - # Keep track of negation, but remove the '!' - is_negate = '' - should_negate = '' - if is.start_with?('!') - is = is.gsub(%r{^!}, '') - is_negate = '!' - end - if should.start_with?('!') - should = should.gsub(%r{^!}, '') - should_negate = '!' - end - - # If 'should' contains anything other than digits or digit range, - # we assume that we have to do a lookup to convert - # to GID - unless should[%r{[0-9]+(-[0-9]+)?}] == should - should = Etc.getgrnam(should).gid - end - - # If 'is' contains anything other than digits or digit range, - # we assume that we have to do a lookup to convert - # to GID - unless is[%r{[0-9]+(-[0-9]+)?}] == is - is = Etc.getgrnam(is).gid - end - - "#{is_negate}#{is}" == "#{should_negate}#{should}" - end - end - - # match mark - newproperty(:match_mark, required_features: :mark) do - desc <<-PUPPETCODE - Match the Netfilter mark value associated with the packet. Accepts either of: - mark/mask or mark. These will be converted to hex if they are not already. - PUPPETCODE - munge do |value| - mark_regex = %r{\A((?:0x)?[0-9A-F]+)(/)?((?:0x)?[0-9A-F]+)?\z}i - match = value.to_s.match(mark_regex) - if match.nil? - raise ArgumentError, 'Match MARK value must be integer or hex between 0 and 0xffffffff' - end - mark = @resource.to_hex32(match[1]) - - # Values that can't be converted to hex. - # Or contain a trailing slash with no mask. - if mark.nil? || (mark && match[2] && match[3].nil?) - raise ArgumentError, 'Match MARK value must be integer or hex between 0 and 0xffffffff' - end - - # There should not be a mask on match_mark - unless match[3].nil? - raise ArgumentError, 'iptables does not support masks on MARK match rules' - end - value = mark - - value - end - end - - newproperty(:set_mark, required_features: :mark) do - desc <<-PUPPETCODE - Set the Netfilter mark value associated with the packet. Accepts either of: - mark/mask or mark. These will be converted to hex if they are not already. - PUPPETCODE - - munge do |value| - int_or_hex = '[a-fA-F0-9x]' - match = value.to_s.match("(#{int_or_hex}+)(/)?(#{int_or_hex}+)?") - mark = @resource.to_hex32(match[1]) - - # Values that can't be converted to hex. - # Or contain a trailing slash with no mask. - if mark.nil? || (mark && match[2] && match[3].nil?) - raise ArgumentError, 'MARK value must be integer or hex between 0 and 0xffffffff' - end - - # Old iptables does not support a mask. New iptables will expect one. - iptables_version = Facter.value('iptables_version') - mask_required = (iptables_version && Puppet::Util::Package.versioncmp(iptables_version, '1.4.1') >= 0) - - if mask_required - if match[3].nil? - value = "#{mark}/0xffffffff" - else - mask = @resource.to_hex32(match[3]) - if mask.nil? - raise ArgumentError, 'MARK mask must be integer or hex between 0 and 0xffffffff' - end - value = "#{mark}/#{mask}" - end - else - unless match[3].nil? - raise ArgumentError, "iptables version #{iptables_version} does not support masks on MARK rules" - end - value = mark - end - - value - end - end - - newproperty(:clamp_mss_to_pmtu, required_features: :iptables) do - desc <<-PUPPETCODE - Sets the clamp mss to pmtu flag. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:set_dscp, required_features: :iptables) do - desc <<-PUPPETCODE - Set DSCP Markings. - PUPPETCODE - end - - newproperty(:set_dscp_class, required_features: :iptables) do - desc <<-PUPPETCODE - This sets the DSCP field according to a predefined DiffServ class. - PUPPETCODE - # iptables uses the cisco DSCP classes as the basis for this flag. Values may be found here: - # 'http://www.cisco.com/c/en/us/support/docs/quality-of-service-qos/qos-packet-marking/10103-dscpvalues.html' - valid_codes = ['af11', 'af12', 'af13', 'af21', 'af22', 'af23', 'af31', 'af32', 'af33', 'af41', 'af42', 'af43', 'cs1', 'cs2', 'cs3', 'cs4', 'cs5', 'cs6', 'cs7', 'ef'] - munge do |value| - unless valid_codes.include? value.downcase - raise ArgumentError, "#{value} is not a valid DSCP Class" - end - value.downcase - end - end - - newproperty(:set_mss, required_features: :iptables) do - desc <<-PUPPETCODE - Sets the TCP MSS value for packets. - PUPPETCODE - end - - newproperty(:pkttype, required_features: :pkttype) do - desc <<-PUPPETCODE - Sets the packet type to match. - PUPPETCODE - - newvalues(:unicast, :broadcast, :multicast) - end - - newproperty(:isfragment, required_features: :isfragment) do - desc <<-PUPPETCODE - Set to true to match tcp fragments (requires type to be set to tcp) - PUPPETCODE - - newvalues(:true, :false) - end + DESC + }, + length: { + type: 'Optional[Pattern[/^([0-9]+)(:)?([0-9]+)?$/]]', + desc: <<-DESC + Sets the length of layer-3 payload to match. - newproperty(:recent, required_features: :recent_limiting) do - desc <<-PUPPETCODE + Example values are: '500', '5:400' + DESC + }, + recent: { + type: "Optional[Enum['set', 'update', 'rcheck', 'remove', '! set', '! update', '! rcheck', '! remove']]", + desc: <<-DESC Enable the recent module. Takes as an argument one of set, update, rcheck or remove. For example: @@ -1623,7 +858,7 @@ def insync?(is) rseconds => 60, rsource => true, rname => 'badguy', - action => 'DROP', + jump => 'DROP', chain => 'FORWARD', } ``` @@ -1638,72 +873,41 @@ def insync?(is) rname => 'badguy', destination => '127.0.0.0/8', iniface => 'eth0', - action => 'DROP', + jump => 'DROP', chain => 'FORWARD', } ``` - PUPPETCODE - - newvalues(:set, :update, :rcheck, :remove) - munge do |value| - _value = '--' + value - end - end - - newproperty(:rdest, required_features: :recent_limiting) do - desc <<-PUPPETCODE - Recent module; add the destination IP address to the list. - Must be boolean true. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:rsource, required_features: :recent_limiting) do - desc <<-PUPPETCODE - Recent module; add the source IP address to the list. - Must be boolean true. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:rname, required_features: :recent_limiting) do - desc <<-PUPPETCODE - Recent module; The name of the list. Takes a string argument. - PUPPETCODE - end - - newproperty(:rseconds, required_features: :recent_limiting) do - desc <<-PUPPETCODE + DESC + }, + rseconds: { + type: 'Optional[Integer[1]]', + desc: <<-DESC Recent module; used in conjunction with one of `recent => 'rcheck'` or `recent => 'update'`. When used, this will narrow the match to only happen when the address is in the list and was seen within the last given number of seconds. - PUPPETCODE - end - - newproperty(:reap, required_features: :recent_limiting) do - desc <<-PUPPETCODE + DESC + }, + reap: { + type: 'Optional[Boolean]', + desc: <<-DESC Recent module; can only be used in conjunction with the `rseconds` attribute. When used, this will cause entries older than 'seconds' to be purged. Must be boolean true. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:rhitcount, required_features: :recent_limiting) do - desc <<-PUPPETCODE + DESC + }, + rhitcount: { + type: 'Optional[Integer[1]]', + desc: <<-DESC Recent module; used in conjunction with `recent => 'update'` or `recent => 'rcheck'. When used, this will narrow the match to only happen when the address is in the list and packets had been received greater than or equal to the given value. - PUPPETCODE - end - - newproperty(:rttl, required_features: :recent_limiting) do - desc <<-PUPPETCODE + DESC + }, + rttl: { + type: 'Optional[Boolean]', + desc: <<-DESC Recent module; may only be used in conjunction with one of `recent => 'rcheck'` or `recent => 'update'`. When used, this will narrow the match to only happen when the address is in the list and the TTL of the current @@ -1711,842 +915,632 @@ def insync?(is) This may be useful if you have problems with people faking their source address in order to DoS you via this module by disallowing others access to your site by sending bogus packets to you. Must be boolean true. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:rpfilter, required_features: :rpfilter, array_matching: :all) do - desc <<-PUPPETCODE - Enable the rpfilter module. - PUPPETCODE - - newvalues(:loose, :validmark, :'accept-local', :invert) - munge do |value| - _value = '--' + value - end - - def insync?(is) - is.to_set == should.to_set - end - end - - newproperty(:socket, required_features: :socket) do - desc <<-PUPPETCODE - If true, matches if an open socket can be found by doing a coket lookup - on the packet. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:ishasmorefrags, required_features: :ishasmorefrags) do - desc <<-PUPPETCODE - If true, matches if the packet has it's 'more fragments' bit set. ipv6. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:islastfrag, required_features: :islastfrag) do - desc <<-PUPPETCODE - If true, matches if the packet is the last fragment. ipv6. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:isfirstfrag, required_features: :isfirstfrag) do - desc <<-PUPPETCODE - If true, matches if the packet is the first fragment. - Sadly cannot be negated. ipv6. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:ipsec_policy, required_features: :ipsec_policy) do - desc <<-PUPPETCODE - Sets the ipsec policy type. May take a combination of arguments for any flags that can be passed to `--pol ipsec` such as: `--strict`, `--reqid 100`, `--next`, `--proto esp`, etc. - PUPPETCODE - - newvalues(:none, :ipsec) - end - - newproperty(:ipsec_dir, required_features: :ipsec_dir) do - desc <<-PUPPETCODE - Sets the ipsec policy direction - PUPPETCODE - - newvalues(:in, :out) - end - - newproperty(:stat_mode) do - desc <<-PUPPETCODE - Set the matching mode for statistic matching. - PUPPETCODE - - newvalues(:nth, :random) - end - - newproperty(:stat_every) do - desc <<-PUPPETCODE - Match one packet every nth packet. Requires `stat_mode => 'nth'` - PUPPETCODE - - validate do |value| - unless %r{^\d+$}.match?(value) - raise ArgumentError, <<-PUPPETCODE - stat_every value must be a digit - PUPPETCODE - end - - unless value.to_i > 0 - raise ArgumentError, <<-PUPPETCODE - stat_every value must be larger than 0 - PUPPETCODE - end - end - end - - newproperty(:stat_packet) do - desc <<-PUPPETCODE - Set the initial counter value for the nth mode. Must be between 0 and the value of `stat_every`. Defaults to 0. Requires `stat_mode => 'nth'` - PUPPETCODE - - newvalues(%r{^\d+$}) - end - - newproperty(:stat_probability) do - desc <<-PUPPETCODE - Set the probability from 0 to 1 for a packet to be randomly matched. It works only with `stat_mode => 'random'`. - PUPPETCODE - - validate do |value| - unless value =~ %r{^([01])\.(\d+)$} - raise ArgumentError, <<-PUPPETCODE - stat_probability must be between 0.0 and 1.0 - PUPPETCODE - end - - if Regexp.last_match(1).to_i == 1 && Regexp.last_match(2).to_i != 0 - raise ArgumentError, <<-PUPPETCODE - start_probability must be between 0.0 and 1.0 - PUPPETCODE - end - end - end - - newproperty(:mask, required_features: :mask) do - desc <<-PUPPETCODE - Sets the mask to use when `recent` is enabled. - PUPPETCODE - end - - newproperty(:gateway, required_features: :iptables) do - desc <<-PUPPETCODE - The TEE target will clone a packet and redirect this clone to another - machine on the local network segment. gateway is the target host's IP. - PUPPETCODE - end - - newproperty(:ipset, required_features: :ipset, array_matching: :all) do - desc <<-PUPPETCODE + DESC + }, + rname: { + type: 'Optional[String[1]]', + desc: <<-DESC + Recent module; The name of the list. + The recent module defaults this to `DEFAULT` when recent is set + DESC + }, + mask: { + type: 'Optional[Pattern[/^\d+\.\d+\.\d+\.\d+$/]]', + desc: <<-DESC + Recent module; sets the mask to use when `recent` is enabled. + The recent module defaults this to `255.255.255.255` when recent is set + DESC + }, + rsource: { + type: 'Optional[Boolean]', + desc: <<-DESC + Recent module; add the source IP address to the list. + Mutually exclusive with `rdest` + The recent module defaults this behaviour to true when recent is set. + DESC + }, + rdest: { + type: 'Optional[Boolean]', + desc: <<-DESC + Recent module; add the destination IP address to the list. + Mutually exclusive with `rsource` + Must be boolean true. + DESC + }, + ipset: { + type: 'Optional[Variant[Pattern[/^(?:!\s)?\w+\s(?:src|dst)(?:,src|,dst)?$/], Array[Pattern[/^(?:!\s)?\w+\s(?:src|dst)(?:,src|,dst)?$/]]]]', + desc: <<-DESC Matches against the specified ipset list. Requires ipset kernel module. Will accept a single element or an array. - The value is the name of the blacklist, followed by a space, and then + The value is the name of the denylist, followed by a space, and then 'src' and/or 'dst' separated by a comma. - For example: 'blacklist src,dst' - PUPPETCODE - - def to_s?(value) - should_to_s(value) - end - - def should_to_s(value) - value = [value] unless value.is_a?(Array) - value.join(', ') - end - end - - newproperty(:checksum_fill, required_features: :iptables) do - desc <<-PUPPETCODE - Compute and fill missing packet checksums. - PUPPETCODE - - newvalues(:true, :false) - end - - newparam(:line) do - desc <<-PUPPETCODE - Read-only property for caching the rule line. - PUPPETCODE - end - - newproperty(:mac_source) do - desc <<-PUPPETCODE - MAC Source - PUPPETCODE - newvalues(%r{^([0-9a-f]{2}[:]){5}([0-9a-f]{2})$}i) - facter_os_name = Facter.value(:os)['name'].downcase - facter_os_release = Facter.value(:os)['release']['major'] - if ['ubuntu-22.04', 'debian-11', 'sles-15'].include?("#{facter_os_name}-#{facter_os_release}") - munge do |value| - _value = value.downcase - end - end - end - - newproperty(:physdev_in, required_features: :iptables) do - desc <<-PUPPETCODE - Match if the packet is entering a bridge from the given interface. - PUPPETCODE - newvalues(%r{^[a-zA-Z0-9\-\._\+]+$}) - end - - newproperty(:physdev_out, required_features: :iptables) do - desc <<-PUPPETCODE - Match if the packet is leaving a bridge via the given interface. - PUPPETCODE - newvalues(%r{^[a-zA-Z0-9\-\._\+]+$}) - end - - newproperty(:physdev_is_bridged, required_features: :iptables) do - desc <<-PUPPETCODE - Match if the packet is transversing a bridge. - PUPPETCODE - newvalues(:true, :false) - end - - newproperty(:physdev_is_in, required_features: :iptables) do - desc <<-PUPPETCODE - Matches if the packet has entered through a bridge interface. - PUPPETCODE - newvalues(:true, :false) - end - - newproperty(:physdev_is_out, required_features: :iptables) do - desc <<-PUPPETCODE - Matches if the packet will leave through a bridge interface. - PUPPETCODE - newvalues(:true, :false) - end - - newproperty(:date_start, required_features: :iptables) do - desc <<-PUPPETCODE - Only match during the given time, which must be in ISO 8601 "T" notation. - The possible time range is 1970-01-01T00:00:00 to 2038-01-19T04:17:07 - PUPPETCODE - end - - newproperty(:date_stop, required_features: :iptables) do - desc <<-PUPPETCODE - Only match during the given time, which must be in ISO 8601 "T" notation. - The possible time range is 1970-01-01T00:00:00 to 2038-01-19T04:17:07 - PUPPETCODE - end - - newproperty(:time_start, required_features: :iptables) do - desc <<-PUPPETCODE - Only match during the given daytime. The possible time range is 00:00:00 to 23:59:59. - Leading zeroes are allowed (e.g. "06:03") and correctly interpreted as base-10. - PUPPETCODE - - munge do |value| - if %r{^([0-9]):}.match?(value) - value = "0#{value}" - end - - if %r{^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$}.match?(value) - value = "#{value}:00" - end - - value - end - end - - newproperty(:time_stop, required_features: :iptables) do - desc <<-PUPPETCODE - Only match during the given daytime. The possible time range is 00:00:00 to 23:59:59. - Leading zeroes are allowed (e.g. "06:03") and correctly interpreted as base-10. - PUPPETCODE - - munge do |value| - if %r{^([0-9]):}.match?(value) - value = "0#{value}" - end - - if %r{^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$}.match?(value) - value = "#{value}:00" - end - - value - end - end + For example: 'denylist src,dst' + To negate simply place a space seperated `!` at the beginning of a value. + Values can de negated independently. + DESC + }, + string: { + type: 'Optional[String[1]]', + desc: <<-DESC + String matching feature. Matches the packet against the pattern + given as an argument. + To negate, add a space seperated `!` to the beginning of the string. + DESC + }, + string_hex: { + type: 'Optional[Pattern[/^(?:!\s)?\|[a-zA-Z0-9\s]+\|$/]]', + desc: <<-DESC + String matching feature. Matches the packet against the pattern + given as an argument. + To negate, add a space seperated `!` to the beginning of the string. + DESC + }, + string_algo: { + type: "Optional[Enum['bm', 'kmp']]", + desc: <<-DESC + String matching feature, pattern matching strategy. + DESC + }, + string_from: { + type: 'Optional[Integer[1]]', + desc: <<-DESC + String matching feature, offset from which we start looking for any matching. + DESC + }, + string_to: { + type: 'Optional[Integer[1]]', + desc: <<-DESC + String matching feature, offset up to which we should scan. + DESC + }, + jump: { + type: 'Optional[Pattern[/^[a-zA-Z0-9_]+$/]]', + desc: <<-DESC + This value for the iptables --jump parameter and the action to perform on a match. Common values are: - newproperty(:month_days, required_features: :iptables) do - desc <<-PUPPETCODE - Only match on the given days of the month. Possible values are 1 to 31. - Note that specifying 31 will of course not match on months which do not have a 31st day; - the same goes for 28- or 29-day February. - PUPPETCODE - - validate do |value| - month = value.to_i - if month >= 1 && month <= 31 - value - else - raise ArgumentError, - 'month_days must be in the range of 1-31' - end - end - end - - newproperty(:week_days, required_features: :iptables) do - desc <<-PUPPETCODE - Only match on the given weekdays. - PUPPETCODE + * ACCEPT - the packet is accepted + * REJECT - the packet is rejected with a suitable ICMP response + * DROP - the packet is dropped - newvalues(:Mon, :Tue, :Wed, :Thu, :Fri, :Sat, :Sun) - end + But can also be on of the following: - newproperty(:time_contiguous, required_features: :iptables) do - desc <<-PUPPETCODE - When time_stop is smaller than time_start value, match this as a single time period instead distinct intervals. - PUPPETCODE + * QUEUE + * RETURN + * DNAT + * SNAT + * LOG + * NFLOG + * NETMAP + * MASQUERADE + * REDIRECT + * MARK + * CT - newvalues(:true, :false) - end + And any valid chain name is also allowed. - newproperty(:kernel_timezone, required_features: :iptables) do - desc <<-PUPPETCODE - Use the kernel timezone instead of UTC to determine whether a packet meets the time regulations. - PUPPETCODE + If you specify no value it will simply match the rule but perform no action. + DESC + }, + goto: { + type: 'Optional[Pattern[/^[a-zA-Z0-9_]+$/]]', + desc: <<-DESC + The value for the iptables --goto parameter. Normal values are: - newvalues(:true, :false) - end + * QUEUE + * RETURN + * DNAT + * SNAT + * LOG + * MASQUERADE + * REDIRECT + * MARK - newproperty(:clusterip_new, required_features: :clusterip) do - desc <<-PUPPETCODE + But any valid chain name is allowed. + DESC + }, + clusterip_new: { + type: 'Optional[Boolean]', + desc: <<-DESC Used with the CLUSTERIP jump target. Create a new ClusterIP. You always have to set this on the first rule for a given ClusterIP. - PUPPETCODE - - newvalues(:true, :false) - end - - newproperty(:clusterip_hashmode, required_features: :clusterip) do - desc <<-PUPPETCODE + This is IPv4 specific. + DESC + }, + clusterip_hashmode: { + type: "Optional[Enum['sourceip', 'sourceip-sourceport', 'sourceip-sourceport-destport']]", + desc: <<-DESC Used with the CLUSTERIP jump target. Specify the hashing mode. - PUPPETCODE - - newvalues(:sourceip, :'sourceip-sourceport', :'sourceip-sourceport-destport') - end - - newproperty(:clusterip_clustermac, required_features: :clusterip) do - desc <<-PUPPETCODE + This is IPv4 specific. + DESC + }, + clusterip_clustermac: { + type: 'Optional[Pattern[/^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$/]]', + desc: <<-DESC Used with the CLUSTERIP jump target. Specify the ClusterIP MAC address. Has to be a link-layer multicast address. - PUPPETCODE - - newvalues(%r{^([0-9a-f]{2}[:]){5}([0-9a-f]{2})$}i) - end - - newproperty(:clusterip_total_nodes, required_features: :clusterip) do - desc <<-PUPPETCODE + This is IPv4 specific. + DESC + }, + clusterip_total_nodes: { + type: 'Optional[Integer[1]]', + desc: <<-DESC Used with the CLUSTERIP jump target. Number of total nodes within this cluster. - PUPPETCODE - - newvalues(%r{\d+}) - end - - newproperty(:clusterip_local_node, required_features: :clusterip) do - desc <<-PUPPETCODE + This is IPv4 specific. + DESC + }, + clusterip_local_node: { + type: 'Optional[Integer[1]]', + desc: <<-DESC Used with the CLUSTERIP jump target. Specify the random seed used for hash initialization. - PUPPETCODE - - newvalues(%r{\d+}) - end - - newproperty(:clusterip_hash_init, required_features: :clusterip) do - desc <<-PUPPETCODE + This is IPv4 specific. + DESC + }, + clusterip_hash_init: { + type: 'Optional[String[1]]', + desc: <<-DESC Used with the CLUSTERIP jump target. Specify the random seed used for hash initialization. - PUPPETCODE - end - - newproperty(:length, required_features: :length) do - desc <<-PUPPETCODE - Sets the length of layer-3 payload to match. - PUPPETCODE - - munge do |value| - match = value.to_s.match('^([0-9]+)(-)?([0-9]+)?$') - if match.nil? - raise ArgumentError, 'Length value must either be an integer or a range' - end - - low = match[1].to_i - unless match[3].nil? - high = match[3].to_i - end - - if (low < 0 || low > 65_535) || \ - (!high.nil? && (high < 0 || high > 65_535 || high < low)) - raise ArgumentError, 'Length values must be between 0 and 65535' - end - - value = low.to_s - unless high.nil? - value << ':' << high.to_s - end - value - end - end - - newproperty(:string, required_features: :string_matching) do - desc <<-PUPPETCODE - String matching feature. Matches the packet against the pattern - given as an argument. - PUPPETCODE - end - - newproperty(:string_hex) do - desc <<-PUPPETCODE - String matching feature. Matches the package against the hex pattern - given as an argument. - PUPPETCODE - munge do |value| - _value = if value.include?('!') - value.split('|').map { |x| x.include?('!') ? x : "|#{x.delete(' ')}|" }.join - else - value.delete(' ') - end - end - end - - newproperty(:string_algo, required_features: :string_matching) do - desc <<-PUPPETCODE - String matching feature, pattern matching strategy. - PUPPETCODE - - newvalues(:bm, :kmp) - end - - newproperty(:string_from, required_features: :string_matching) do - desc <<-PUPPETCODE - String matching feature, offset from which we start looking for any matching. - PUPPETCODE - end - - newproperty(:string_to, required_features: :string_matching) do - desc <<-PUPPETCODE - String matching feature, offset up to which we should scan. - PUPPETCODE - end - - newproperty(:queue_num, required_features: :queue_num) do - desc <<-PUPPETCODE + This is IPv4 specific. + DESC + }, + queue_num: { + type: 'Optional[Integer[1]]', + desc: <<-DESC Used with NFQUEUE jump target. What queue number to send packets to - PUPPETCODE - munge do |value| - match = value.to_s.match('^([0-9])*$') - if match.nil? - raise ArgumentError, 'queue_num must be an integer' - end - - if match[1].to_i > 65_535 || match[1].to_i < 0 - raise ArgumentError, 'queue_num must be between 0 and 65535' - end - value - end - end - - newproperty(:queue_bypass, required_features: :queue_bypass) do - desc <<-PUPPETCODE - Used with NFQUEUE jump target + DESC + }, + queue_bypass: { + type: 'Optional[Boolean]', + desc: <<-DESC Allow packets to bypass :queue_num if userspace process is not listening - PUPPETCODE - newvalues(:true, :false) - end + DESC + }, + nflog_group: { + type: 'Optional[Integer[1, 65535]]', + desc: <<-DESC + Used with the jump target NFLOG. + The netlink group (0 - 2^16-1) to which packets are (only applicable + for nfnetlink_log). Defaults to 0. + DESC + }, + nflog_prefix: { + type: 'Optional[String]', + desc: <<-DESC + Used with the jump target NFLOG. + A prefix string to include in the log message, up to 64 characters long, + useful for distinguishing messages in the logs. + DESC + }, + nflog_range: { + type: 'Optional[Integer[1]]', + desc: <<-DESC + Used with the jump target NFLOG. + This has never worked, use nflog_size instead. + DESC + }, + nflog_size: { + type: 'Optional[Integer[1]]', + desc: <<-DESC + Used with the jump target NFLOG. + The number of bytes to be copied to userspace (only applicable for nfnetlink_log). + nfnetlink_log instances may specify their own size, this option overrides it. + DESC + }, + nflog_threshold: { + type: 'Optional[Integer[1]]', + desc: <<-DESC + Used with the jump target NFLOG. + Number of packets to queue inside the kernel before sending them to userspace + (only applicable for nfnetlink_log). Higher values result in less overhead + per packet, but increase delay until the packets reach userspace. Defaults to 1. + DESC + }, + gateway: { + type: 'Optional[Pattern[/^(\d+.\d+.\d+.\d+|\w+:\w+::\w+)$/]]', + desc: <<-DESC + The TEE target will clone a packet and redirect this clone to another + machine on the local network segment. + Gateway is the target host's IP. + DESC + }, + clamp_mss_to_pmtu: { + type: 'Optional[Boolean]', + desc: <<-DESC + Sets the clamp mss to pmtu flag. + DESC + }, + set_mss: { + type: 'Optional[Integer[1]]', + desc: <<-DESC + Sets the TCP MSS value for packets. + DESC + }, + set_dscp: { + type: 'Optional[String[1]]', + desc: <<-DESC + Set DSCP Markings. + DESC + }, + set_dscp_class: { + type: "Optional[Enum['af11', 'af12', 'af13', 'af21', 'af22', 'af23', 'af31', 'af32', 'af33', 'af41', 'af42', 'af43', 'cs1', 'cs2', 'cs3', 'cs4', 'cs5', 'cs6', 'cs7', 'ef']]", + desc: <<-DESC + This sets the DSCP field according to a predefined DiffServ class. + DESC + }, + todest: { + type: 'Optional[String[1]]', + desc: <<-DESC + When using jump => "DNAT" you can specify the new destination address using this paramter. + Can specify a single new destination IP address or an inclusive range of IP addresses. + Optionally a port or a port range with a possible follow up baseport can be provided. + Input structure: [ipaddr[-ipaddr]][:port[-port[/baseport]]] + DESC + }, + tosource: { + type: 'Optional[String[1]]', + desc: <<-DESC + When using jump => "SNAT" you can specify the new source address using this paramter. + Can specify a single new destination IP address or an inclusive range of IP addresses. + Input structure: [ipaddr[-ipaddr]][:port[-port]] + DESC + }, + toports: { + type: 'Optional[Pattern[/^\d+(?:-\d+)?$/]]', + desc: <<-DESC + For REDIRECT/MASQUERADE this is the port that will replace the destination/source port. + Can specify a single new port or an inclusive range of ports. + DESC + }, + to: { + type: 'Optional[String[1]]', + desc: <<-DESC + For NETMAP this will replace the destination IP + DESC + }, + checksum_fill: { + type: 'Optional[Boolean]', + desc: <<-DESC + Compute and fill missing packet checksums. + DESC + }, + random_fully: { + type: 'Optional[Boolean]', + desc: <<-DESC + When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" this boolean will enable fully randomized port mapping. + DESC + }, + random: { + type: 'Optional[Boolean]', + desc: <<-DESC + When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT" this boolean will enable randomized port mapping. + DESC + }, + log_prefix: { + type: 'Optional[String[1]]', + desc: <<-DESC + When combined with jump => "LOG" specifies the log prefix to use when logging. + DESC + }, + log_level: { + type: 'Optional[Variant[Integer[0,7],String[1]]]', + desc: <<-DESC + When combined with jump => "LOG" specifies the system log level to log to. + + Note: log level 4/warn is the default setting and as such it is not returned by iptables-save. + As a result, explicitly setting `log_level` to this can result in idempotency errors. + DESC + }, + log_uid: { + type: 'Optional[Boolean]', + desc: <<-DESC + When combined with jump => "LOG" specifies the uid of the process making the connection. + DESC + }, + log_tcp_sequence: { + type: 'Optional[Boolean]', + desc: <<-DESC + When combined with jump => "LOG" enables logging of the TCP sequence numbers. + DESC + }, + log_tcp_options: { + type: 'Optional[Boolean]', + desc: <<-DESC + When combined with jump => "LOG" logging of the TCP packet header. + DESC + }, + log_ip_options: { + type: 'Optional[Boolean]', + desc: <<-DESC + When combined with jump => "LOG" logging of the TCP IP/IPv6 packet header. + DESC + }, + reject: { + type: "Optional[Enum['icmp-net-unreachable', 'icmp-host-unreachable', 'icmp-port-unreachable', 'icmp-proto-unreachable', + 'icmp-net-prohibited', 'icmp-host-prohibited', 'icmp-admin-prohibited', 'icmp6-no-route', 'no-route', + 'icmp6-adm-prohibited', 'adm-prohibited', 'icmp6-addr-unreachable', 'addr-unreach', 'icmp6-port-unreachable']]", + desc: <<-DESC + When combined with jump => "REJECT" you can specify a different icmp response to be sent back to the packet sender. + Valid values differ depending on if the protocol is `IPv4` or `IPv6`. + IPv4 allows: icmp-net-unreachable, icmp-host-unreachable, icmp-port-unreachable, icmp-proto-unreachable, icmp-net-prohibited, + icmp-host-prohibited, or icmp-admin-prohibited. + IPv6 allows: icmp6-no-route, no-route, icmp6-adm-prohibited, adm-prohibited, icmp6-addr-unreachable, addr-unreach, or icmp6-port-unreachable. + DESC + }, + set_mark: { + type: 'Optional[Pattern[/^[a-fA-F0-9x]+(?:\/[a-fA-F0-9x]+)?$/]]', + desc: <<-DESC + Set the Netfilter mark value associated with the packet. Accepts either of mark/mask or mark. + These will be converted to hex if they are not already. + DESC + }, + match_mark: { + type: 'Optional[Pattern[/^(?:!\s)?[a-fA-F0-9x]+$/]]', + desc: <<-DESC + Match the Netfilter mark value associated with the packet, accepts a mark. + This value will be converted to hex if it is not already. + This value can be negated by adding a space seperated `!` to the beginning. + DESC + }, + mss: { + type: 'Optional[Pattern[/^(?:!\s)?\d+(?:\:\d+)?$/]]', + desc: <<-DESC + Match a given TCP MSS value or range. + This value can be negated by adding a space seperated `!` to the beginning. + DESC + }, + connlimit_upto: { + type: 'Optional[Integer]', + desc: <<-DESC + Connection limiting value for matched connections below or equal to n. + DESC + }, + connlimit_above: { + type: 'Optional[Integer]', + desc: <<-DESC + Connection limiting value for matched connections above n. + DESC + }, + connlimit_mask: { + type: 'Optional[Integer[0,128]]', + desc: <<-DESC + Connection limiting by subnet mask for matched connections. + IPv4: 0-32 + IPv6: 0-128 + DESC + }, + connmark: { + type: 'Optional[Pattern[/^(?:!\s)?[a-fA-F0-9x]+$/]]', + desc: <<-DESC + Match the Netfilter mark value associated with the packet, accepts a mark. + This value will be converted to hex if it is not already. + This value can be negated by adding a space seperated `!` to the beginning. + DESC + }, + time_start: { + type: 'Optional[Pattern[/^([0-9]|[0-1][0-9]|2[0-3])\:[0-5][0-9](?:\:[0-5][0-9])?/]]', + desc: <<-DESC + Only match during the given daytime. The possible time range is 00:00:00 to 23:59:59. + Leading zeroes are allowed (e.g. "06:03") and correctly interpreted as base-10. + DESC + }, + time_stop: { + type: 'Optional[Pattern[/^([0-9]|[0-1][0-9]|2[0-3])\:[0-5][0-9](?:\:[0-5][0-9])?/]]', + desc: <<-DESC + Only match during the given daytime. The possible time range is 00:00:00 to 23:59:59. + Leading zeroes are allowed (e.g. "06:03") and correctly interpreted as base-10. + DESC + }, + month_days: { + type: 'Optional[Variant[Integer[0,31], Array[Integer[0,31]]]]', + desc: <<-DESC + Only match on the given days of the month. Possible values are 1 to 31. + Note that specifying 31 will of course not match on months which do not have a 31st day; + the same goes for 28-day or 29-day February. + + Can be passed either as a single value or an array of values: + month_days => 5, + month_days => [5, 9, 23], + DESC + }, + week_days: { + type: "Optional[Variant[Enum['Mon','Tue','Wed','Thu','Fri','Sat','Sun'], Array[Enum['Mon','Tue','Wed','Thu','Fri','Sat','Sun']]]]", + desc: <<-DESC + Only match on the given weekdays. - newproperty(:src_cc) do - desc <<-PUPPETCODE + Can be passed either as a single value or an array of values: + week_days => 'Mon', + week_days => ['Mon', 'Tue', 'Wed'], + DESC + }, + date_start: { + type: 'Optional[Pattern[/^[0-9]{4}\-(?:0[0-9]|1[0-2])\-(?:[0-2][0-9]|3[0-1])T(?:[0-1][0-9]|2[0-3])\:[0-5][0-9]\:[0-5][0-9]$/]]', + desc: <<-DESC + Only match during the given time, which must be in ISO 8601 "T" notation. + The possible time range is 1970-01-01T00:00:00 to 2038-01-19T04:17:07 + DESC + }, + date_stop: { + type: 'Optional[Pattern[/^[0-9]{4}\-(?:0[0-9]|1[0-2])\-(?:[0-2][0-9]|3[0-1])T(?:[0-1][0-9]|2[0-3])\:[0-5][0-9]\:[0-5][0-9]$/]]', + desc: <<-DESC + Only match during the given time, which must be in ISO 8601 "T" notation. + The possible time range is 1970-01-01T00:00:00 to 2038-01-19T04:17:07 + DESC + }, + time_contiguous: { + type: 'Optional[Boolean]', + desc: <<-DESC + When time_stop is smaller than time_start value, match this as a single time period instead distinct intervals. + DESC + }, + kernel_timezone: { + type: 'Optional[Boolean]', + desc: <<-DESC + Use the kernel timezone instead of UTC to determine whether a packet meets the time regulations. + DESC + }, + u32: { + type: 'Optional[Pattern[/^0x[0-9a-fA-F]+&0x[0-9a-fA-F]+=0x[0-9a-fA-F]+(?::0x[0-9a-fA-F]+)?(?:&&0x[0-9a-fA-F]+&0x[0-9a-fA-F]+=0x[0-9a-fA-F]+(?::0x[0-9a-fA-F]+)?)*$/]]', + desc: <<-DESC + Enable the u32 module. Takes as an argument one of set, update, + rcheck or remove. For example: + firewall { '032 u32 test': + ensure => present, + table => 'mangle', + chain => 'PREROUTING', + u32 => '0x4&0x1fff=0x0&&0x0&0xf000000=0x5000000', + jump => 'DROP', + } + DESC + }, + src_cc: { + type: 'Optional[Pattern[/^[A-Z]{2}(,[A-Z]{2})*$/]]', + desc: <<-DESC src attribute for the module geoip - PUPPETCODE - newvalues(%r{^[A-Z]{2}(,[A-Z]{2})*$}) - end - - newproperty(:dst_cc) do - desc <<-PUPPETCODE + DESC + }, + dst_cc: { + type: 'Optional[Pattern[/^[A-Z]{2}(,[A-Z]{2})*$/]]', + desc: <<-DESC dst attribute for the module geoip - PUPPETCODE - newvalues(%r{^[A-Z]{2}(,[A-Z]{2})*$}) - end - - newproperty(:hashlimit_name) do - desc <<-PUPPETCODE - The name for the /proc/net/ipt_hashlimit/foo entry. - This parameter is required. - PUPPETCODE - end - - newproperty(:hashlimit_upto) do - desc <<-PUPPETCODE + DESC + }, + hashlimit_upto: { + type: 'Optional[Pattern[/^\d+(?:\/(?:sec|min|hour|day))?$/]]', + desc: <<-DESC Match if the rate is below or equal to amount/quantum. It is specified either as a number, with an optional time quantum suffix (the default is 3/hour), or as amountb/second (number of bytes per second). - This parameter or hashlimit_above is required. - Allowed forms are '40','40/second','40/minute','40/hour','40/day'. - PUPPETCODE - end - - newproperty(:hashlimit_above) do - desc <<-PUPPETCODE + This parameter or `hashlimit_above` and `hashlimit_name` are required when setting any other hashlimit values. + Allowed forms are '40','40/sec','40/min','40/hour','40/day'. + DESC + }, + hashlimit_above: { + type: 'Optional[Pattern[/^\d+(?:\/(?:sec|min|hour|day))?$/]]', + desc: <<-DESC Match if the rate is above amount/quantum. - This parameter or hashlimit_upto is required. - Allowed forms are '40','40/second','40/minute','40/hour','40/day'. - PUPPETCODE - end - - newproperty(:hashlimit_burst) do - desc <<-PUPPETCODE - Maximum initial number of packets to match: this number gets recharged by one every time the limit specified above is not reached, up to this number; the default is 5. When byte-based rate matching is requested, this option specifies the amount of bytes that can exceed the given rate. This option should be used with caution -- if the entry expires, the burst value is reset too. - PUPPETCODE - newvalue(%r{^\d+$}) - end - - newproperty(:hashlimit_mode) do - desc <<-PUPPETCODE - A comma-separated list of objects to take into consideration. If no --hashlimit-mode option is given, hashlimit acts like limit, but at the expensive of doing the hash housekeeping. + This parameter or `hashlimit_upto` and `hashlimit_name` are required when setting any other hashlimit values. + Allowed forms are '40','40/sec','40/min','40/hour','40/day'. + DESC + }, + hashlimit_name: { + type: 'Optional[String[1]]', + desc: <<-DESC + The name for the /proc/net/ipt_hashlimit/foo entry. + This parameter and either `hashlimit_upto` or `hashlimit_above` are required when setting any other hashlimit values. + DESC + }, + hashlimit_burst: { + type: 'Optional[Integer[1]]', + desc: <<-DESC + Maximum initial number of packets to match: this number gets recharged by one every time the limit specified above is not reached, up to this number; the default is 5. + When byte-based rate matching is requested, this option specifies the amount of bytes that can exceed the given rate. + This option should be used with caution -- if the entry expires, the burst value is reset too. + DESC + }, + hashlimit_mode: { + type: 'Optional[Pattern[/^(?:srcip|srcport|dstip|dstport)(?:\,(?:srcip|srcport|dstip|dstport))*$/]]', + desc: <<-DESC + A comma-separated list of objects to take into consideration. + If no --hashlimit-mode option is given, hashlimit acts like limit, but at the expensive of doing the hash housekeeping. Allowed values are: srcip, srcport, dstip, dstport - PUPPETCODE - end - - newproperty(:hashlimit_srcmask) do - desc <<-PUPPETCODE - When --hashlimit-mode srcip is used, all source addresses encountered will be grouped according to the given prefix length and the so-created subnet will be subject to hashlimit. prefix must be between (inclusive) 0 and 32. Note that --hashlimit-srcmask 0 is basically doing the same thing as not specifying srcip for --hashlimit-mode, but is technically more expensive. - PUPPETCODE - end - - newproperty(:hashlimit_dstmask) do - desc <<-PUPPETCODE - Like --hashlimit-srcmask, but for destination addresses. - PUPPETCODE - end - - newproperty(:hashlimit_htable_size) do - desc <<-PUPPETCODE + DESC + }, + hashlimit_srcmask: { + type: 'Optional[Integer[0,32]]', + desc: <<-DESC + When --hashlimit-mode srcip is used, all source addresses encountered will be grouped according to the given prefix length + and the so-created subnet will be subject to hashlimit. + Prefix must be between (inclusive) 0 and 32. + Note that --hashlimit-srcmask 0 is basically doing the same thing as not specifying srcip for --hashlimit-mode, but is technically more expensive. + DESC + }, + hashlimit_dstmask: { + type: 'Optional[Integer[0,32]]', + desc: <<-DESC + When --hashlimit-mode srcip is used, all destination addresses encountered will be grouped according to the given prefix length + and the so-created subnet will be subject to hashlimit. + Prefix must be between (inclusive) 0 and 32. + Note that --hashlimit-dstmask 0 is basically doing the same thing as not specifying srcip for --hashlimit-mode, but is technically more expensive. + DESC + }, + hashlimit_htable_size: { + type: 'Optional[Integer]', + desc: <<-DESC The number of buckets of the hash table - PUPPETCODE - end - - newproperty(:hashlimit_htable_max) do - desc <<-PUPPETCODE + DESC + }, + hashlimit_htable_max: { + type: 'Optional[Integer]', + desc: <<-DESC Maximum entries in the hash. - PUPPETCODE - end - - newproperty(:hashlimit_htable_expire) do - desc <<-PUPPETCODE + DESC + }, + hashlimit_htable_expire: { + type: 'Optional[Integer]', + desc: <<-DESC After how many milliseconds do hash entries expire. - PUPPETCODE - end - - newproperty(:hashlimit_htable_gcinterval) do - desc <<-PUPPETCODE + DESC + }, + hashlimit_htable_gcinterval: { + type: 'Optional[Integer]', + desc: <<-DESC How many milliseconds between garbage collection intervals. - PUPPETCODE - end - - newproperty(:bytecode, required_features: :iptables) do - desc <<-PUPPETCODE + DESC + }, + bytecode: { + type: 'Optional[String[1]]', + desc: <<-DESC Match using Linux Socket Filter. Expects a BPF program in decimal format. This is the format generated by the nfbpf_compile utility. - PUPPETCODE - end - - newproperty(:ipvs, required_features: :ipvs) do - desc <<-PUPPETCODE - Indicates that the current packet belongs to an IPVS connection. - PUPPETCODE - newvalues(:true, :false) - end - - newproperty(:zone, required_features: :ct_target) do - desc <<-PUPPETCODE + DESC + }, + ipvs: { + type: 'Optional[Boolean]', + desc: <<-DESC + Match using Linux Socket Filter. Expects a BPF program in decimal format. + This is the format generated by the nfbpf_compile utility. + DESC + }, + zone: { + type: 'Optional[Integer]', + desc: <<-DESC Assign this packet to zone id and only have lookups done in that zone. - PUPPETCODE - end - - newproperty(:helper, required_features: :ct_target) do - desc <<-PUPPETCODE + DESC + }, + helper: { + type: 'Optional[String[1]]', + desc: <<-DESC Invoke the nf_conntrack_xxx helper module for this packet. - PUPPETCODE - end - - newproperty(:cgroup) do - desc <<-PUPPETCODE + DESC + }, + cgroup: { + type: 'Optional[String[1]]', + desc: <<-DESC Matches against the net_cls cgroup ID of the packet. - PUPPETCODE - end - - newproperty(:notrack, required_features: :ct_target) do - # use this parameter with latest version of iptables - desc <<-PUPPETCODE - Invoke the disable connection tracking for this packet. - This parameter can be used with iptables version >= 1.8.3 - PUPPETCODE - newvalues(:true, :false) - end - - newproperty(:condition, required_features: :condition) do - desc <<-PUPPETCODE + + To negate add a space seperate `!` to the beginning of the string + DESC + }, + rpfilter: { + type: "Optional[Variant[Enum['loose', 'validmark', 'accept-local', 'invert'], Array[Enum['loose', 'validmark', 'accept-local', 'invert']]]]", + desc: <<-DESC + Enable the rpfilter module. + DESC + }, + condition: { + type: 'Optional[String[1]]', + desc: <<-DESC Match on boolean value (0/1) stored in /proc/net/nf_condition/name. - PUPPETCODE - validate do |value| - unless value.is_a?(String) - raise ArgumentError, <<-PUPPETCODE - Condition must be a string. - PUPPETCODE - end - end - end - - autorequire(:firewallchain) do - reqs = [] - protocol = nil - - case value(:provider) - when :iptables - protocol = 'IPv4' - when :ip6tables - protocol = 'IPv6' - end - - unless protocol.nil? - table = value(:table) - main_chains = ['INPUT', 'OUTPUT', 'FORWARD'] - [value(:chain), value(:jump)].each do |chain| - reqs << "#{chain}:#{table}:#{protocol}" unless chain.nil? || (main_chains.include?(chain) && table == :filter) - end - end - - reqs - end - - # Classes would be a better abstraction, pending: - # http://projects.puppetlabs.com/issues/19001 - autorequire(:package) do - case value(:provider) - when :iptables, :ip6tables - ['iptables', 'iptables-persistent', 'iptables-services'] - else - [] - end - end - - autorequire(:service) do - case value(:provider) - when :iptables, :ip6tables - ['firewalld', 'iptables', 'ip6tables', 'iptables-persistent', 'netfilter-persistent'] - else - [] - end - end - - # On RHEL 7 this needs to be threaded correctly to manage SE Linux permissions after persisting the rules - autobefore(:file) do - ['/etc/sysconfig/iptables', '/etc/sysconfig/ip6tables'] - end - - validate do # rubocop:disable Metrics/BlockLength - debug('[validate]') - - # TODO: this is put here to skip validation if ensure is not set. This - # is because there is a revalidation stage called later where the values - # are not set correctly. I tried tracing it - but have put in this - # workaround instead to skip. Must get to the bottom of this. - unless value(:ensure) - return - end - - # First we make sure the chains and tables are valid combinations - if value(:table).to_s == 'filter' && - value(:chain) =~ %r{PREROUTING|POSTROUTING} - - raise "PREROUTING and POSTROUTING cannot be used in table 'filter'" - end - - if value(:table).to_s == 'nat' && value(:chain) =~ %r{INPUT|FORWARD} - raise "INPUT and FORWARD cannot be used in table 'nat'" - end - - if value(:table).to_s == 'raw' && - value(:chain) =~ %r{INPUT|FORWARD|POSTROUTING} - - raise 'INPUT, FORWARD and POSTROUTING cannot be used in table raw' - end - - # Now we analyse the individual properties to make sure they apply to - # the correct combinations. - if value(:uid) - unless %r{OUTPUT|POSTROUTING}.match?(value(:chain).to_s) - raise 'Parameter uid only applies to chains ' \ - 'OUTPUT,POSTROUTING' - end - end - - if value(:gid) - unless %r{OUTPUT|POSTROUTING}.match?(value(:chain).to_s) - raise 'Parameter gid only applies to chains ' \ - 'OUTPUT,POSTROUTING' - end - end - - if value(:set_mark) - unless value(:jump).to_s.include?('MARK') && - value(:table).to_s.include?('mangle') - raise 'Parameter set_mark only applies to ' \ - 'the mangle table and when jump => MARK' - end - end - - if value(:dport) - unless %r{tcp|udp|sctp}.match?(value(:proto).to_s) - raise '[%s] Parameter dport only applies to sctp, tcp and udp ' \ - 'protocols. Current protocol is [%s] and dport is [%s]' % - [value(:name), should(:proto), should(:dport)] - end - end - - if value(:jump).to_s == 'DSCP' - unless value(:set_dscp) || value(:set_dscp_class) - raise 'When using jump => DSCP, the set_dscp or set_dscp_class property is required' - end - end - - if value(:jump).to_s == 'TCPMSS' - unless value(:set_mss) || value(:clamp_mss_to_pmtu) - raise 'When using jump => TCPMSS, the set_mss or clamp_mss_to_pmtu property is required' - end - end - - if value(:jump).to_s == 'TEE' - unless value(:gateway) - raise 'When using jump => TEE, the gateway property is required' - end - end - - if value(:jump).to_s == 'DNAT' - unless %r{nat}.match?(value(:table).to_s) - raise 'Parameter jump => DNAT only applies to table => nat' - end - - unless value(:todest) - raise 'Parameter jump => DNAT must have todest parameter' - end - end - - if value(:jump).to_s == 'SNAT' - unless %r{nat}.match?(value(:table).to_s) - raise 'Parameter jump => SNAT only applies to table => nat' - end - - unless value(:tosource) - raise 'Parameter jump => SNAT must have tosource parameter' - end - end - - if value(:jump).to_s == 'MASQUERADE' - unless %r{nat}.match?(value(:table).to_s) - raise 'Parameter jump => MASQUERADE only applies to table => nat' - end - end - - if value(:log_prefix) || value(:log_level) || value(:log_uid) || - value(:log_tcp_sequence) || value(:log_tcp_options) || value(:log_ip_options) == :true - unless value(:jump).to_s == 'LOG' - raise 'Parameter log_prefix, log_level, log_tcp_sequence, log_tcp_options, log_ip_options and log_uid require jump => LOG' - end - end - - if value(:burst) && !value(:limit) - raise 'burst makes no sense without limit' - end - - if value(:action) && value(:jump) - raise "Only one of the parameters 'action' and 'jump' can be set" - end - - if value(:connlimit_mask) && !value(:connlimit_above) - raise "Parameter 'connlimit_mask' requires 'connlimit_above'" - end - - if value(:mask) && !value(:recent) - raise 'Mask can only be set if recent is enabled.' - end - - [:stat_packet, :stat_every, :stat_probability].each do |param| - if value(param) && !value(:stat_mode) - raise "Parameter '#{param}' requires 'stat_mode' to be set" - end - end - - if value(:stat_packet) && value(:stat_mode) != :nth - raise "Parameter 'stat_packet' requires 'stat_mode' to be set to 'nth'" - end - - if value(:stat_every) && value(:stat_mode) != :nth - raise "Parameter 'stat_every' requires 'stat_mode' to be set to 'nth'" - end - - if value(:stat_probability) && value(:stat_mode) != :random - raise "Parameter 'stat_probability' requires 'stat_mode' to be set to 'random'" - end - - if value(:checksum_fill) == :true - unless value(:jump).to_s == 'CHECKSUM' && value(:table).to_s == 'mangle' - raise 'Parameter checksum_fill requires jump => CHECKSUM and table => mangle' - end - end - - if value(:queue_num) || value(:queue_bypass) == :true - unless value(:jump).to_s == 'NFQUEUE' - raise 'Paramter queue_number and queue_bypass require jump => NFQUEUE' - end - end - - if value(:hashlimit_name) - unless value(:hashlimit_upto) || value(:hashlimit_above) - raise 'Either hashlimit_upto or hashlimit_above are required' - end - end - - if value(:zone) - unless value(:jump).to_s == 'CT' - raise 'Parameter zone requires jump => CT' - end - end - - if value(:helper) - unless value(:jump).to_s == 'CT' - raise 'Parameter helper requires jump => CT' - end - end - - if value(:notrack) - unless value(:jump).to_s == 'CT' - raise 'Parameter notrack requires jump => CT' - end - end - - if value(:jump).to_s == 'CT' - unless %r{raw}.match?(value(:table).to_s) - raise 'Parameter jump => CT only applies to table => raw' - end - end - end -end + DESC + }, + notrack: { + type: 'Optional[Boolean]', + desc: <<-DESC + Invoke the disable connection tracking for this packet. + This parameter can be used with iptables version >= 1.8.3 + DESC + } + }, +) diff --git a/lib/puppet/type/firewallchain.rb b/lib/puppet/type/firewallchain.rb index 45c61ef27..6c86cccc8 100644 --- a/lib/puppet/type/firewallchain.rb +++ b/lib/puppet/type/firewallchain.rb @@ -1,107 +1,45 @@ # frozen_string_literal: true -# This is a workaround for bug: #4248 whereby ruby files outside of the normal -# provider/type path do not load until pluginsync has occured on the puppet server -# -# In this case I'm trying the relative path first, then falling back to normal -# mechanisms. This should be fixed in future versions of puppet but it looks -# like we'll need to maintain this for some time perhaps. -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..')) -require 'puppet/util/firewall' - -Puppet::Type.newtype(:firewallchain) do - include Puppet::Util::Firewall - - @doc = <<-PUPPETCODE - @summary - This type provides the capability to manage rule chains for firewalls. - - Currently this supports only iptables, ip6tables and ebtables on Linux. And - provides support for setting the default policy on chains and tables that - allow it. - - **Autorequires:** - If Puppet is managing the iptables, iptables-persistent, or iptables-services packages, - and the provider is iptables_chain, the firewall resource will autorequire - those packages to ensure that any required binaries are installed. - - #### Providers - * iptables_chain is the only provider that supports firewallchain. - - #### Features - * iptables_chain: The provider provides iptables chain features. - * policy: Default policy (inbuilt chains only). - PUPPETCODE - - feature :iptables_chain, 'The provider provides iptables chain features.' - feature :policy, 'Default policy (inbuilt chains only)' - - ensurable do - defaultvalues - defaultto :present - end - - newparam(:name) do - desc <<-PUPPETCODE - The canonical name of the chain. - - For iptables the format must be {chain}:{table}:{protocol}. - PUPPETCODE - isnamevar - - validate do |value| - if value !~ NAME_FORMAT - raise ArgumentError, 'Inbuilt chains must be in the form {chain}:{table}:{protocol} where {table} is one of filter,' \ - ' nat, mangle, raw, rawpost, broute, security or empty (alias for filter), chain can be anything without colons' \ - ' or one of PREROUTING, POSTROUTING, BROUTING, INPUT, FORWARD, OUTPUT for the inbuilt chains, and {protocol} being' \ - " IPv4, IPv6, ethernet (ethernet bridging) got '#{value}' table:'#{Regexp.last_match(1)}' chain:'#{Regexp.last_match(2)}' protocol:'#{Regexp.last_match(3)}'" - else - chain = Regexp.last_match(1) - table = Regexp.last_match(2) - protocol = Regexp.last_match(3) - case table - when 'filter' - if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(chain) - raise ArgumentError, "INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table 'filter'" - end - when 'mangle' - if chain =~ INTERNAL_CHAINS && chain == 'BROUTING' - raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table 'mangle'" - end - when 'nat' - if %r{^(BROUTING|FORWARD)$}.match?(chain) - raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, and OUTPUT are the only inbuilt chains that can be used in table 'nat'" - end - if Gem::Version.new(Facter['kernelmajversion'].value.dup) < Gem::Version.new('3.7') && protocol =~ %r{^(IP(v6)?)?$} - raise ArgumentError, "table nat isn't valid in IPv6. You must specify ':IPv4' as the name suffix" - end - when 'raw' - if %r{^(POSTROUTING|BROUTING|INPUT|FORWARD)$}.match?(chain) - raise ArgumentError, 'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\'' - end - when 'broute' - if protocol != 'ethernet' - raise ArgumentError, 'BROUTE is only valid with protocol \'ethernet\'' - end - if %r{^PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT$}.match?(chain) - raise ArgumentError, 'BROUTING is the only inbuilt chain allowed on on table \'broute\'' - end - when 'security' - if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(chain) - raise ArgumentError, "INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table 'security'" - end - end - if chain == 'BROUTING' && (protocol != 'ethernet' || table != 'broute') - raise ArgumentError, 'BROUTING is the only inbuilt chain allowed on on table \'BROUTE\' with protocol \'ethernet\' i.e. \'broute:BROUTING:enternet\'' - end - end - end - end - - newproperty(:policy) do - desc <<-PUPPETCODE - This is the action to when the end of the chain is reached. - It can only be set on inbuilt chains (INPUT, FORWARD, OUTPUT, +# lib/puppet/type/firewallchain.rb +require 'puppet/resource_api' + +Puppet::ResourceApi.register_type( + name: 'firewallchain', + features: ['custom_generate', 'custom_insync'], + docs: <<-DESC, + This type provides the capability to manage rule chains for firewalls. + + Currently this supports only iptables, ip6tables and ebtables on Linux. And + provides support for setting the default policy on chains and tables that + allow it. + + #### Providers + * iptables_chain is the only provider that supports firewallchain. + + #### Features + * iptables_chain: The provider provides iptables chain features. + * policy: Default policy (inbuilt chains only). + DESC + attributes: { + ensure: { + type: 'Enum[present, absent]', + default: 'present', + desc: <<-DESC + Whether this chain should be present or absent on the target system. + Setting this to absent will first remove all rules associated with this chain and then delete the chain itself. + Inbuilt chains however will merely remove any added rules and, if it has been changed, return their policy to the default. + DESC + }, + name: { + type: 'Pattern[/^(?:\S+):(?:nat|mangle|filter|raw|rawpost|broute|security):(?:IP(?:v[46])?|ethernet)$/]', + desc: 'The canonical name of the chain with the required format being `{chain}:{table}:{protocol}`.', + behaviour: :namevar + }, + policy: { + type: "Optional[Enum['accept', 'drop', 'queue', 'return']]", + desc: <<-DESC + This action to take when the end of the chain is reached. + This can only be set on inbuilt chains (i.e. INPUT, FORWARD, OUTPUT, PREROUTING, POSTROUTING) and can be one of: * accept - the packet is accepted @@ -109,30 +47,17 @@ * queue - the packet is passed userspace * return - the packet is returned to calling (jump) queue or the default of inbuilt chains - PUPPETCODE - newvalues(:accept, :drop, :queue, :return) - defaultto do - # ethernet chain have an ACCEPT default while other haven't got an - # allowed value - if @resource[:name] =~ %r{:ethernet$} - :accept - else - nil - end - end - end - - newparam(:purge, boolean: true) do - desc <<-PUPPETCODE - Purge unmanaged firewall rules in this chain - PUPPETCODE - newvalues(false, true) - defaultto false - end - - newparam(:ignore) do - desc <<-PUPPETCODE - Regex to perform on firewall rules to exempt unmanaged rules from purging (when enabled). + DESC + }, + purge: { + type: 'Boolean', + default: false, + desc: 'Whether or not to purge unmanaged rules in this chain' + }, + ignore: { + type: 'Optional[Variant[String[1], Array[String[1]]]]', + desc: <<-DESC + Regex to perform on firewall rules to exempt unmanaged rules from purging. This is matched against the output of `iptables-save`. This can be a single regex, or an array of them. @@ -152,111 +77,15 @@ ], } ``` - PUPPETCODE - - validate do |value| - unless value.is_a?(Array) || value.is_a?(String) || value == false - devfail 'Ignore must be a string or an Array' - end - end - munge do |patterns| # convert into an array of {Regex}es - patterns = [patterns] if patterns.is_a?(String) - patterns.map { |p| Regexp.new(p) } - end - end - - newparam(:ignore_foreign, boolean: true) do - desc <<-PUPPETCODE + DESC + }, + ignore_foreign: { + type: 'Boolean', + default: false, + desc: <<-DESC Ignore rules that do not match the puppet title pattern "^\d+[[:graph:][:space:]]" when purging unmanaged firewall rules in this chain. This can be used to ignore rules that were not put in by puppet. Beware that nothing keeps other systems from configuring firewall rules with a comment that starts with digits, and is indistinguishable from puppet-configured rules. - PUPPETCODE - newvalues(false, true) - defaultto false - end - - # Classes would be a better abstraction, pending: - # http://projects.puppetlabs.com/issues/19001 - autorequire(:package) do - case value(:provider) - when :iptables_chain - ['iptables', 'iptables-persistent', 'iptables-services'] - else - [] - end - end - - autorequire(:service) do - case value(:provider) - when :iptables, :ip6tables - ['firewalld', 'iptables', 'ip6tables', 'iptables-persistent', 'netfilter-persistent'] - else - [] - end - end - - validate do - debug('[validate]') - - value(:name).match(NAME_FORMAT) - chain = Regexp.last_match(1) - table = Regexp.last_match(2) - protocol = Regexp.last_match(3) - - # Check that we're not removing an internal chain - if chain =~ INTERNAL_CHAINS && value(:ensure) == :absent - raise 'Cannot remove in-built chains' - end - - if value(:policy).nil? && protocol == 'ethernet' - raise 'you must set a non-empty policy on all ethernet table chains' - end - - # Check that we're not setting a policy on a user chain - if chain !~ INTERNAL_CHAINS && - !value(:policy).nil? && - protocol != 'ethernet' - - raise "policy can only be set on in-built chains (with the exception of ethernet chains) (table:#{table} chain:#{chain} protocol:#{protocol})" - end - - # no DROP policy on nat table - if table == 'nat' && - value(:policy) == :drop - - raise 'The "nat" table is not intended for filtering, the use of DROP is therefore inhibited' - end - end - - def generate - return [] unless purge? - - value(:name).match(NAME_FORMAT) - chain = Regexp.last_match(1) - table = Regexp.last_match(2) - protocol = Regexp.last_match(3) - - provider = case protocol - when 'IPv4' - :iptables - when 'IPv6' - :ip6tables - end - - # gather a list of all rules present on the system - rules_resources = Puppet::Type.type(:firewall).instances - - # Keep only rules in this chain - rules_resources.delete_if { |res| (res[:provider] != provider || res.provider.properties[:table].to_s != table || res.provider.properties[:chain] != chain) } - - # Remove rules which match our ignore filter - rules_resources.delete_if { |res| value(:ignore).find_index { |f| res.provider.properties[:line].match(f) } } if value(:ignore) - - # Remove rules that were (presumably) not put in by puppet - rules_resources.delete_if { |res| res.provider.properties[:name].match(%r{^(\d+)[[:graph:][:space:]]})[1].to_i >= 9000 } if value(:ignore_foreign) == :true - - # We mark all remaining rules for deletion, and then let the catalog override us on rules which should be present - rules_resources.each { |res| res[:ensure] = :absent } - - rules_resources - end -end + DESC + } + }, +) diff --git a/lib/puppet/util/firewall.rb b/lib/puppet/util/firewall.rb deleted file mode 100644 index 0801b13c0..000000000 --- a/lib/puppet/util/firewall.rb +++ /dev/null @@ -1,262 +0,0 @@ -# frozen_string_literal: true - -require 'socket' -require 'resolv' -require 'puppet/util/ipcidr' - -# Util module for puppetlabs-firewall -module Puppet::Util::Firewall - # Translate the symbolic names for icmp packet types to integers - def icmp_name_to_number(value_icmp, protocol) - if %r{\d{1,2}$}.match?(value_icmp) - value_icmp - elsif protocol == 'inet' - case value_icmp - when 'echo-reply' then '0' - when 'destination-unreachable' then '3' - when 'source-quench' then '4' - when 'redirect' then '6' - when 'echo-request' then '8' - when 'router-advertisement' then '9' - when 'router-solicitation' then '10' - when 'time-exceeded' then '11' - when 'parameter-problem' then '12' - when 'timestamp-request' then '13' - when 'timestamp-reply' then '14' - when 'address-mask-request' then '17' - when 'address-mask-reply' then '18' - else nil - end - elsif protocol == 'inet6' - case value_icmp - when 'destination-unreachable' then '1' - when 'too-big' then '2' - when 'time-exceeded' then '3' - when 'parameter-problem' then '4' - when 'echo-request' then '128' - when 'echo-reply' then '129' - when 'router-solicitation' then '133' - when 'router-advertisement' then '134' - when 'neighbour-solicitation' then '135' - when 'neighbour-advertisement' then '136' - when 'redirect' then '137' - else nil - end - else - raise ArgumentError, "unsupported protocol family '#{protocol}'" - end - end - - # Convert log_level names to their respective numbers - def log_level_name_to_number(value) - if %r{\A[0-7]\z}.match?(value) - value - else - case value - when 'panic' then '0' - when 'alert' then '1' - when 'crit' then '2' - when 'err' then '3' - when 'error' then '3' - when 'warn' then '4' - when 'warning' then '4' - when 'not' then '5' - when 'notice' then '5' - when 'info' then '6' - when 'debug' then '7' - else nil - end - end - end - - # This method takes a string and a protocol and attempts to convert - # it to a port number if valid. - # - # If the string already contains a port number or perhaps a range of ports - # in the format 22:1000 for example, it simply returns the string and does - # nothing. - def string_to_port(value, proto) - proto = proto.to_s - unless %r{^(tcp|udp)$}.match?(proto) - proto = 'tcp' - end - - m = value.to_s.match(%r{^(!\s+)?(\S+)}) - return "#{m[1]}#{m[2]}" if %r{^\d+(-\d+)?$}.match?(m[2]) - "#{m[1]}#{Socket.getservbyname(m[2], proto)}" - end - - # Takes an address and protocol and returns the address in CIDR notation. - # - # The protocol is only used when the address is a hostname. - # - # If the address is: - # - # - A hostname: - # It will be resolved - # - An IPv4 address: - # It will be qualified with a /32 CIDR notation - # - An IPv6 address: - # It will be qualified with a /128 CIDR notation - # - An IP address with a CIDR notation: - # It will be normalised - # - An IP address with a dotted-quad netmask: - # It will be converted to CIDR notation - # - Any address with a resulting prefix length of zero: - # It will return nil which is equivilent to not specifying an address - # - def host_to_ip(value, proto = nil) - begin - value = Puppet::Util::IPCidr.new(value) - rescue - family = case proto - when :IPv4 - Socket::AF_INET - when :IPv6 - Socket::AF_INET6 - when nil - raise ArgumentError, 'Proto must be specified for a hostname' - else - raise ArgumentError, "Unsupported address family: #{proto}" - end - - new_value = nil - Resolv.each_address(value) do |addr| - begin # rubocop:disable Style/RedundantBegin - new_value = Puppet::Util::IPCidr.new(addr, family) - break - rescue # looking for the one that works # rubocop:disable Lint/SuppressedException - end - end - - raise "Failed to resolve hostname #{value}" if new_value.nil? - value = new_value - end - - return nil if value.prefixlen.zero? - value.cidr - end - - # Takes an address mask and protocol and converts the host portion to CIDR - # notation. - # - # This takes into account you can negate a mask but follows all rules - # defined in host_to_ip for the host/address part. - # - def host_to_mask(value, proto) - match = value.match %r{(!)\s?(.*)$} - return host_to_ip(value, proto) unless match - - cidr = host_to_ip(match[2], proto) - return nil if cidr.nil? - "#{match[1]} #{cidr}" - end - - # Validates the argument is int or hex, and returns valid hex - # conversion of the value or nil otherwise. - def to_hex32(value) - begin - value = Integer(value) - if value.between?(0, 0xffffffff) - return '0x' + value.to_s(16) - end - rescue ArgumentError - # pass - end - nil - end - - def persist_iptables(proto) - debug('[persist_iptables]') - - # Basic normalisation for older Facter - os_key = Facter.value(:osfamily) - os_key ||= case Facter.value(:operatingsystem) - when 'RedHat', 'CentOS', 'Fedora', 'Scientific', 'SL', 'SLC', 'Ascendos', 'CloudLinux', - 'PSBM', 'OracleLinux', 'OVS', 'OEL', 'Amazon', 'XenServer', 'VirtuozzoLinux', 'Rocky', 'AlmaLinux' - 'RedHat' - when 'Debian', 'Ubuntu' - 'Debian' - else - Facter.value(:operatingsystem) - end - - # Older iptables-persistent doesn't provide save action. - if os_key == 'Debian' - # We need to call flush to clear Facter cache as it's possible the cached value will be nil due to the fact - # that the iptables-persistent package was potentially installed after the initial Fact gathering. - fact = Facter.fact(:iptables_persistent_version) - fact.flush if fact.respond_to?(:flush) - persist_ver = fact.value - if persist_ver && Puppet::Util::Package.versioncmp(persist_ver, '0.5.0') < 0 - os_key = 'Debian_manual' - end - end - - # Fedora 15 and newer use systemd to persist iptable rules - if os_key == 'RedHat' && Facter.value(:operatingsystem) == 'Fedora' && Facter.value(:operatingsystemrelease).to_i >= 15 - os_key = 'Fedora' - end - - # RHEL 7 and newer also use systemd to persist iptable rules - if os_key == 'RedHat' && ['RedHat', 'CentOS', 'Scientific', 'SL', 'SLC', 'Ascendos', 'CloudLinux', 'PSBM', 'OracleLinux', 'OVS', 'OEL', 'XenServer', 'VirtuozzoLinux', 'Rocky', 'AlmaLinux'] - .include?(Facter.value(:operatingsystem)) && Facter.value(:operatingsystemrelease).to_i >= 7 - os_key = 'Fedora' - end - - cmd = case os_key.to_sym - when :RedHat - case proto.to_sym - when :IPv4 - ['/sbin/service', 'iptables', 'save'] - when :IPv6 - ['/sbin/service', 'ip6tables', 'save'] - end - when :Fedora - case proto.to_sym - when :IPv4 - ['/usr/libexec/iptables/iptables.init', 'save'] - when :IPv6 - ['/usr/libexec/iptables/ip6tables.init', 'save'] - end - when :Debian - case proto.to_sym - when :IPv4, :IPv6 - if persist_ver && Puppet::Util::Package.versioncmp(persist_ver, '1.0') > 0 - ['/usr/sbin/service', 'netfilter-persistent', 'save'] - else - ['/usr/sbin/service', 'iptables-persistent', 'save'] - end - end - when :Debian_manual - case proto.to_sym - when :IPv4 - ['/bin/sh', '-c', '/sbin/iptables-save > /etc/iptables/rules'] - end - when :Archlinux - case proto.to_sym - when :IPv4 - ['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/iptables/iptables.rules'] - when :IPv6 - ['/bin/sh', '-c', '/usr/sbin/ip6tables-save > /etc/iptables/ip6tables.rules'] - end - when :Suse - case proto.to_sym - when :IPv4 - ['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/sysconfig/iptables'] - end - end - - # Catch unsupported OSs from the case statement above. - if cmd.nil? - debug('firewall: Rule persistence is not supported for this type/OS') - return - end - - begin - execute(cmd) - rescue Puppet::ExecutionFailure => detail - warning("Unable to persist firewall rules: #{detail}") - end - end -end diff --git a/lib/puppet/util/ipcidr.rb b/lib/puppet_x/puppetlabs/firewall/ipcidr.rb similarity index 84% rename from lib/puppet/util/ipcidr.rb rename to lib/puppet_x/puppetlabs/firewall/ipcidr.rb index c9c6cd04f..fe6b67420 100644 --- a/lib/puppet/util/ipcidr.rb +++ b/lib/puppet_x/puppetlabs/firewall/ipcidr.rb @@ -1,14 +1,16 @@ # frozen_string_literal: true +require 'puppet_x' require 'ipaddr' -module Puppet::Util +module PuppetX::Firewall # rubocop:disable Style/ClassAndModuleChildren # IPCidr object wrapper for IPAddr class IPCidr < IPAddr def initialize(ipaddr, family = Socket::AF_UNSPEC) super(ipaddr, family) rescue ArgumentError => e - raise ArgumentError, "Invalid address from IPAddr.new: #{ipaddr}" if %r{invalid address}.match?(e.message) + raise ArgumentError, "Invalid address from IPAddr.new: #{ipaddr}" if e.message.include?('invalid address') + raise e end @@ -26,6 +28,7 @@ def prefixlen raise 'unsupported address family' end return Regexp.last_match(1).length if %r{\A(1*)(0*)\z} =~ (@mask_addr & m).to_s(2) + raise 'bad addr_mask format' end diff --git a/lib/puppet_x/puppetlabs/firewall/utility.rb b/lib/puppet_x/puppetlabs/firewall/utility.rb new file mode 100644 index 000000000..56e03f727 --- /dev/null +++ b/lib/puppet_x/puppetlabs/firewall/utility.rb @@ -0,0 +1,293 @@ +# frozen_string_literal: true + +require 'puppet_x' +require 'socket' +require 'resolv' +require 'puppet_x/puppetlabs/firewall/ipcidr' + +module PuppetX::Firewall # rubocop:disable Style/ClassAndModuleChildren + # A utility class meant to contain re-usable code + class Utility + # Save any current iptables changes so they are retained upon restart + def self.persist_iptables(context, name, protocol) + os_key = Facter.value('os')['family'] + cmd = case os_key + when 'RedHat' + case protocol + when 'IPv4', 'iptables' + ['/usr/libexec/iptables/iptables.init', 'save'] + when 'IPv6', 'ip6tables' + ['/usr/libexec/iptables/ip6tables.init', 'save'] + end + when 'Debian' + fact = Facter.fact(:iptables_persistent_version) + fact.flush if fact.respond_to?(:flush) + persist_ver = fact.value + + case protocol + when 'IPv4', 'IPv6', 'iptables', 'ip6tables' + if persist_ver && Puppet::Util::Package.versioncmp(persist_ver, '1.0').positive? + ['/usr/sbin/service', 'netfilter-persistent', 'save'] + else + ['/usr/sbin/service', 'iptables-persistent', 'save'] + end + end + when 'Archlinux' + case protocol + when 'IPv4', 'iptables' + ['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/iptables/iptables.rules'] + when 'IPv6', 'ip6tables' + ['/bin/sh', '-c', '/usr/sbin/ip6tables-save > /etc/iptables/ip6tables.rules'] + end + when 'Suse' + case protocol + when 'IPv4', 'iptables' + ['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/sysconfig/iptables'] + end + else + # Catch unsupported OSs + debug('firewall: Rule persistence is not supported for this type/OS') + return + end + + # Run the persist command within a rescue block + begin + context.notice("Ensuring changes to '#{name}' persist") + Puppet::Provider.execute(cmd) + rescue Puppet::ExecutionFailure => e + warn "Unable to persist firewall rules: #{e}" + end + end + + # @api private + def self.create_absent(namevar, title) + result = if title.is_a? Hash + title.dup + else + { namevar => title } + end + result[:ensure] = 'absent' + result + end + + # Takes an address and protocol and returns the address in CIDR notation. + # + # The protocol is only used when the address is a hostname. + # + # If the address is: + # + # - A hostname: + # It will be resolved + # - An IPv4 address: + # It will be qualified with a /32 CIDR notation + # - An IPv6 address: + # It will be qualified with a /128 CIDR notation + # - An IP address with a CIDR notation: + # It will be normalised + # - An IP address with a dotted-quad netmask: + # It will be converted to CIDR notation + # - Any address with a resulting prefix length of zero: + # It will return nil which is equivilent to not specifying an address + # + def self.host_to_ip(value, proto = nil) + begin + value = PuppetX::Firewall::IPCidr.new(value) + rescue StandardError + family = case proto + when 'IPv4', 'iptables' + Socket::AF_INET + when 'IPv6', 'ip6tables' + Socket::AF_INET6 + when nil + raise ArgumentError, 'Proto must be specified for a hostname' + else + raise ArgumentError, "Unsupported address family: #{proto}" + end + + new_value = nil + Resolv.each_address(value) do |addr| + begin # rubocop:disable Style/RedundantBegin + new_value = PuppetX::Firewall::IPCidr.new(addr, family) + break + rescue StandardError # looking for the one that works # rubocop:disable Lint/SuppressedException + end + end + + raise "Failed to resolve hostname #{value}" if new_value.nil? + + value = new_value + end + + return nil if value.prefixlen.zero? + + value.cidr + end + + # Takes an address mask and protocol and converts the host portion to CIDR + # notation. + # + # This takes into account you can negate a mask but follows all rules + # defined in host_to_ip for the host/address part. + # + def self.host_to_mask(value, proto) + match = value.match %r{(!)\s?(.*)$} + return PuppetX::Firewall::Utility.host_to_ip(value, proto) unless match + + cidr = PuppetX::Firewall::Utility.host_to_ip(match[2], proto) + return nil if cidr.nil? + + "#{match[1]} #{cidr}" + end + + # Translate the symbolic names for icmp packet types to integers + def self.icmp_name_to_number(value_icmp, protocol) + if value_icmp.to_s.match?(%r{^\d+$}) + value_icmp.to_s + elsif ['IPv4', 'iptables'].include?(protocol) + # https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml + case value_icmp + when 'echo-reply' then '0' + when 'destination-unreachable' then '3' + when 'source-quench' then '4' + when 'redirect' then '6' + when 'echo-request' then '8' + when 'router-advertisement' then '9' + when 'router-solicitation' then '10' + when 'time-exceeded' then '11' + when 'parameter-problem' then '12' + when 'timestamp-request' then '13' + when 'timestamp-reply' then '14' + when 'address-mask-request' then '17' + when 'address-mask-reply' then '18' + else nil + end + elsif ['IPv6', 'ip6tables'].include?(protocol) + # https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml + case value_icmp + when 'destination-unreachable' then '1' + when 'too-big' then '2' + when 'time-exceeded' then '3' + when 'parameter-problem' then '4' + when 'echo-request' then '128' + when 'echo-reply' then '129' + when 'router-solicitation' then '133' + when 'router-advertisement' then '134' + when 'neighbour-solicitation' then '135' + when 'neighbour-advertisement' then '136' + when 'redirect' then '137' + else nil + end + else + raise ArgumentError, "unsupported protocol family '#{protocol}'" + end + end + + # Convert log_level names to their respective numbers + # https://www.iana.org/assignments/syslog-parameters/syslog-parameters.xhtml + def self.log_level_name_to_number(value) + if value.to_s.match?(%r{^[0-7]$}) + value.to_s + else + case value + when 'panic' then '0' + when 'alert' then '1' + when 'crit' then '2' + when 'err', 'error' then '3' + when 'warn', 'warning' then '4' + when 'not', 'notice' then '5' + when 'info' then '6' + when 'debug' then '7' + else nil + end + end + end + + # Validates the argument is int or hex, and returns valid hex + # conversion of the value or nil otherwise. + def self.to_hex32(value) + begin + value = Integer(value) + return "0x#{value.to_s(16)}" if value.between?(0, 0xffffffff) + rescue ArgumentError + # pass + end + nil + end + + # Accepts a valid mark or mark/mask and returns them in the valid + # hexidecimal format. + # USed for set_mark + def self.mark_mask_to_hex(value) + match = value.to_s.match(%r{([a-fA-F0-9x]+)/?([a-fA-F0-9x]+)?}) + mark = PuppetX::Firewall::Utility.to_hex32(match[1]) + return "#{mark}/0xffffffff" if match[2].nil? + + mask = PuppetX::Firewall::Utility.to_hex32(match[2]) + "#{mark}/#{mask}" + end + + # Accepts a valid mark and returns them in the valid hexidecimal format. + # Accounts for negation. + # Used for match_mark / connmark + def self.mark_to_hex(value) + match = value.to_s.match(%r{^(!\s)?([a-fA-F0-9x]+)}) + mask = PuppetX::Firewall::Utility.to_hex32(match[2]) + return mask if match[1].nil? + + "! #{mask}" + end + + # Converts a given number to its protocol keyword + # https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml + def self.proto_number_to_name(value) + return value if %r{^(?:!\s)?([a-z])}.match?(value) + + match = value.to_s.match(%r{^(!\s)?(.*)}) + keyword = case match[2] + when '1' then 'icmp' + when '2' then 'igmp' + when '4' then 'ipencap' + when '6' then 'tcp' + when '7' then 'cbt' + when '17' then 'udp' + when '47' then 'gre' + when '50' then 'esp' + when '51' then 'ah' + when '89' then 'ospf' + when '103' then 'pim' + when '112' then 'vrrp' + when '132' then 'sctp' + else raise ArgumentError, "Unsupported proto number: #{value}" + end + "#{match[1]}#{keyword}" + end + + # Converts a given number to its dscp class name + # https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml + def self.dscp_number_to_class(value) + case value + when '0x0a' then 'af11' + when '0x0c' then 'af12' + when '0x0e' then 'af13' + when '0x12' then 'af21' + when '0x14' then 'af22' + when '0x16' then 'af23' + when '0x1a' then 'af31' + when '0x1c' then 'af32' + when '0x1e' then 'af33' + when '0x22' then 'af41' + when '0x24' then 'af42' + when '0x26' then 'af43' + when '0x08' then 'cs1' + when '0x10' then 'cs2' + when '0x18' then 'cs3' + when '0x20' then 'cs4' + when '0x28' then 'cs5' + when '0x30' then 'cs6' + when '0x38' then 'cs7' + when '0x2e' then 'ef' + else nil + end + end + end +end diff --git a/manifests/linux/redhat.pp b/manifests/linux/redhat.pp index 77ca5f201..c54d942f1 100644 --- a/manifests/linux/redhat.pp +++ b/manifests/linux/redhat.pp @@ -139,6 +139,21 @@ #lint:ignore:quoted_booleans 'true',true: { case $facts['os']['name'] { + 'RedHat': { + case $facts['os']['release']['full'] { + /^7\..*/: { + $seluser = 'unconfined_u' + $seltype = 'system_conf_t' + } + default : { + $seluser = 'system_u' + $seltype = 'system_conf_t' + } + } + + File<| title == "/etc/sysconfig/${service_name}" |> { seluser => $seluser, seltype => $seltype } + File<| title == "/etc/sysconfig/${service_name_v6}" |> { seluser => $seluser, seltype => $seltype } + } 'CentOS': { case $facts['os']['release']['full'] { /^6\..*/: { diff --git a/spec/acceptance/class_spec.rb b/spec/acceptance/class_spec.rb index df7fde40f..0e66285a3 100644 --- a/spec/acceptance/class_spec.rb +++ b/spec/acceptance/class_spec.rb @@ -4,22 +4,20 @@ describe 'firewall class' do before(:all) do - if os[:family] == 'ubuntu' || os[:family] == 'debian' - update_profile_file - end + update_profile_file if os[:family] == 'ubuntu' || os[:family] == 'debian' end - it 'runs successfully', unless: os[:family] == 'redhat' && os[:release].to_i == 6 do + it 'runs successfully' do pp = "class { 'firewall': }" idempotent_apply(pp) end - it 'ensure => stopped:', unless: os[:family] == 'redhat' && os[:release].to_i == 6 do + it 'ensure => stopped:' do pp = "class { 'firewall': ensure => stopped }" idempotent_apply(pp) end - it 'ensure => running:', unless: os[:family] == 'redhat' && os[:release].to_i == 6 do + it 'ensure => running:' do pp = "class { 'firewall': ensure => running }" idempotent_apply(pp) end diff --git a/spec/acceptance/firewall_attributes_exceptions_spec.rb b/spec/acceptance/firewall_attributes_exceptions_spec.rb index 4033acb48..992c145c8 100644 --- a/spec/acceptance/firewall_attributes_exceptions_spec.rb +++ b/spec/acceptance/firewall_attributes_exceptions_spec.rb @@ -28,13 +28,13 @@ pp = <<-PUPPETCODE class { '::firewall': } firewall { '102 - test': - action => 'accept', + jump => 'accept', bytecode => '4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0', chain => 'OUTPUT', proto => 'all', table => 'filter', } - PUPPETCODE + PUPPETCODE it 'applies' do apply_manifest(pp, catch_failures: true) end @@ -55,18 +55,18 @@ class { '::firewall': } firewall { '561 - test': proto => tcp, dport => '9999561-562', - action => accept, + jump => accept, } PUPPETCODE it 'applies' do apply_manifest(pp22, expect_failures: true) do |r| - expect(r.stderr).to match(%r{invalid port\/service `9999561' specified}) + expect(r.stderr).to match(%r{invalid port/service `9999561' specified}) end end it 'contains the rule' do run_shell('iptables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m multiport --dports 9999561-562 -m comment --comment "560 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m multiport --dports 9999561-562 -m comment --comment "560 - test" -j ACCEPT}) end end end @@ -80,7 +80,7 @@ class { '::firewall': } ensure => present, proto => tcp, dport => '555', - action => accept, + jump => accept, } PUPPETCODE it 'applies' do @@ -89,7 +89,7 @@ class { '::firewall': } it 'contains the rule' do run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 555 -m comment --comment "555 - test" -j ACCEPT}) + expect(r.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 555 -m comment --comment "555 - test" -j ACCEPT}) end end end @@ -101,7 +101,7 @@ class { '::firewall': } ensure => absent, proto => tcp, dport => '555', - action => accept, + jump => accept, } PUPPETCODE it 'applies' do @@ -110,31 +110,12 @@ class { '::firewall': } it 'does not contain the rule' do run_shell('iptables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m multiport --dports 555 -m comment --comment "555 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 555 -m comment --comment "555 - test" -j ACCEPT}) end end end end - # describe 'firewall inverting' do - # context 'when inverting partial array rules' do - # pp2 = <<-PUPPETCODE - # class { '::firewall': } - # firewall { '603 drop 80,443 traffic': - # chain => 'INPUT', - # action => 'drop', - # proto => 'tcp', - # sport => ['! http', '443'], - # } - # PUPPETCODE - # it 'raises a failure' do - # apply_manifest(pp2, expect_failures: true) do |r| - # expect(r.stderr).to match(%r{is not prefixed}) - # end - # end - # end - # end - describe 'isfragment' do describe 'adding a rule' do before(:all) do @@ -162,13 +143,15 @@ class { '::firewall': } let(:result) { run_shell('iptables-save') } it 'when unset' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m comment --comment "803 - test"}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m comment --comment "803 - test"}) end + it 'when set to true' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -f -m comment --comment "804 - test"}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -f -m comment --comment "804 - test"}) end + it 'when set to false' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m comment --comment "805 - test"}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m comment --comment "805 - test"}) end end @@ -214,147 +197,150 @@ class { '::firewall': } let(:result) { run_shell('iptables-save') } it 'when unset or false' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m comment --comment "806 - test"}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m comment --comment "806 - test"}) end + it 'when unset or false and current value is true' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -f -m comment --comment "807 - test"}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -f -m comment --comment "807 - test"}) end + it 'when set to true and current value is false' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m comment --comment "808 - test"}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m comment --comment "808 - test"}) end + it 'when set to true and current value is true' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -f -m comment --comment "809 - test"}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -f -m comment --comment "809 - test"}) end end end - # describe 'firewall isfragment property' do - # before :all do - # iptables_flush_all_tables - # ip6tables_flush_all_tables - # end - - # shared_examples 'is idempotent' do |value, line_match| - # pp1 = <<-PUPPETCODE - # class { '::firewall': } - # firewall { '597 - test': - # ensure => present, - # proto => 'tcp', - # #{value} - # } - # PUPPETCODE - # it "changes the value to #{value}" do - # apply_manifest(pp1, catch_failures: true, expect_failures: true) - # apply_manifest(pp1, catch_changes: true, expect_failures: true) - - # run_shell('iptables-save') do |r| - # expect(r.stdout).to match(%r{#{line_match}}) - # end - # end - # end - - # shared_examples "doesn't change" do |value, line_match| - # pp2 = <<-PUPPETCODE - # class { '::firewall': } - # firewall { '597 - test': - # ensure => present, - # proto => 'tcp', - # #{value} - # } - # PUPPETCODE - # it "doesn't change the value to #{value}" do - # apply_manifest(pp2, catch_changes: true, expect_failures: true) - - # run_shell('iptables-save') do |r| - # expect(r.stdout).to match(%r{#{line_match}}) - # end - # end - # end - - # describe 'adding a rule' do - # context 'when unset' do - # before :all do - # iptables_flush_all_tables - # end - # it_behaves_like 'is idempotent', '', %r{-A INPUT -p tcp -m comment --comment "597 - test"} - # end - # context 'when set to true' do - # before :all do - # iptables_flush_all_tables - # end - # it_behaves_like 'is idempotent', 'isfragment => true,', %r{-A INPUT -p tcp -f -m comment --comment "597 - test"} - # end - # context 'when set to false' do - # before :all do - # iptables_flush_all_tables - # end - # it_behaves_like 'is idempotent', 'isfragment => false,', %r{-A INPUT -p tcp -m comment --comment "597 - test"} - # end - # end - - # describe 'editing a rule and current value is false' do - # context 'when unset or false' do - # before :each do - # iptables_flush_all_tables - # run_shell('iptables -A INPUT -p tcp -m comment --comment "597 - test"') - # end - # it_behaves_like "doesn't change", 'isfragment => false,', %r{-A INPUT -p tcp -m comment --comment "597 - test"} - # end - # context 'when unset or false and current value is true' do - # before :each do - # iptables_flush_all_tables - # run_shell('iptables -A INPUT -p tcp -m comment --comment "597 - test"') - # end - # it_behaves_like 'is idempotent', 'isfragment => true,', %r{-A INPUT -p tcp -f -m comment --comment "597 - test"} - # end - - # context 'when set to true and current value is false' do - # before :each do - # iptables_flush_all_tables - # run_shell('iptables -A INPUT -p tcp -f -m comment --comment "597 - test"') - # end - # it_behaves_like 'is idempotent', 'isfragment => false,', %r{-A INPUT -p tcp -m comment --comment "597 - test"} - # end - # context 'when set to trueand current value is true' do - # before :each do - # iptables_flush_all_tables - # run_shell('iptables -A INPUT -p tcp -f -m comment --comment "597 - test"') - # end - # it_behaves_like "doesn't change", 'isfragment => true,', %r{-A INPUT -p tcp -f -m comment --comment "597 - test"} - # end - # end - # end + describe 'firewall isfragment property' do + before :all do + iptables_flush_all_tables + ip6tables_flush_all_tables + end + + shared_examples 'is idempotent' do |value, line_match| + pp1 = <<-PUPPETCODE + class { '::firewall': } + firewall { '597 - test': + ensure => present, + proto => 'tcp', + #{value} + } + PUPPETCODE + it "changes the value to #{value}" do + idempotent_apply(pp1) + + run_shell('iptables-save') do |r| + expect(r.stdout).to match(%r{#{line_match}}) + end + end + end + + shared_examples "doesn't change" do |value, line_match| + pp2 = <<-PUPPETCODE + class { '::firewall': } + firewall { '597 - test': + ensure => present, + proto => 'tcp', + #{value} + } + PUPPETCODE + it "doesn't change the value to #{value}" do + apply_manifest(pp2, catch_changes: true) + + run_shell('iptables-save') do |r| + expect(r.stdout).to match(%r{#{line_match}}) + end + end + end + + describe 'adding a rule' do + context 'when unset' do + before :all do + iptables_flush_all_tables + end + + it_behaves_like 'is idempotent', '', %r{-A INPUT -p (tcp|6) -m comment --comment "597 - test"} + end + + context 'when set to true' do + before :all do + iptables_flush_all_tables + end + + it_behaves_like 'is idempotent', 'isfragment => true,', %r{-A INPUT -p (tcp|6) -f -m comment --comment "597 - test"} + end + + context 'when set to false' do + before :all do + iptables_flush_all_tables + end + + it_behaves_like 'is idempotent', 'isfragment => false,', %r{-A INPUT -p (tcp|6) -m comment --comment "597 - test"} + end + end + + describe 'editing a rule and current value is false' do + context 'when unset or false' do + before :each do + iptables_flush_all_tables + run_shell('iptables -A INPUT -p tcp -m comment --comment "597 - test"') + end + + it_behaves_like "doesn't change", 'isfragment => false,', %r{-A INPUT -p (tcp|6) -m comment --comment "597 - test"} + end + + context 'when unset or false and current value is true' do + before :each do + iptables_flush_all_tables + run_shell('iptables -A INPUT -p tcp -m comment --comment "597 - test"') + end + + it_behaves_like 'is idempotent', 'isfragment => true,', %r{-A INPUT -p (tcp|6) -f -m comment --comment "597 - test"} + end + + context 'when set to true and current value is false' do + before :each do + iptables_flush_all_tables + run_shell('iptables -A INPUT -p tcp -f -m comment --comment "597 - test"') + end + + it_behaves_like 'is idempotent', 'isfragment => false,', %r{-A INPUT -p (tcp|6) -m comment --comment "597 - test"} + end + + context 'when set to trueand current value is true' do + before :each do + iptables_flush_all_tables + run_shell('iptables -A INPUT -p tcp -f -m comment --comment "597 - test"') + end + + it_behaves_like "doesn't change", 'isfragment => true,', %r{-A INPUT -p (tcp|6) -f -m comment --comment "597 - test"} + end + end + end describe 'mac_source' do context 'when 0A:1B:3C:4D:5E:6F' do - # On RHEL 9 this must be lower case, on all others it must be upper case - mac_source = if os[:family] == 'redhat' && os[:release].start_with?('9') - '0a:1b:3c:4d:5e:6f' - else - '0A:1B:3C:4D:5E:6F' - end pp88 = <<-PUPPETCODE class { '::firewall': } firewall { '610 - test': ensure => present, source => '10.1.5.28/32', - mac_source => '#{mac_source}', + mac_source => '0A:1B:3C:4D:5E:6F', chain => 'INPUT', } PUPPETCODE it 'applies' do idempotent_apply(pp88) end + it 'contains the rule' do run_shell('iptables-save') do |r| - if os[:family] == 'redhat' && os[:release].start_with?('5') - expect(r.stdout).to match(%r{-A INPUT -s 10.1.5.28 -p tcp -m mac --mac-source 0A:1B:3C:4D:5E:6F -m comment --comment "610 - test"}) - else - expect(r.stdout).to match(%r{-A INPUT -s 10.1.5.28\/(32|255\.255\.255\.255) -p tcp -m mac --mac-source 0(a|A):1(b|B):3(c|C):4(d|D):5(e|E):6(f|F) -m comment --comment "610 - test"}) - end + expect(r.stdout).to match(%r{-A INPUT -s 10.1.5.28/(32|255\.255\.255\.255) -p (tcp|6) -m mac --mac-source 0(a|A):1(b|B):3(c|C):4(d|D):5(e|E):6(f|F) -m comment --comment "610 - test"}) end end - # rubocop:enable RSpec/ExampleLength : Cannot reduce lines to required size end end @@ -367,7 +353,7 @@ class { '::firewall': } PUPPETCODE it 'fails' do apply_manifest(pp, expect_failures: true) do |r| - expect(r.stderr).to match(%r{Rule sorting error}) + expect(r.stderr).to match(%r{Rule name cannot start with 9000-9999}) end end end @@ -487,30 +473,6 @@ class {'::firewall': } end end - describe 'dport' do - context 'when invalid dports' do - pp25 = <<-PUPPETCODE - class { '::firewall': } - firewall { '562 - test': - proto => tcp, - dport => '9999562-563', - action => accept, - } - PUPPETCODE - it 'applies' do - apply_manifest(pp25, expect_failures: true) do |r| - expect(r.stderr).to match(%r{invalid port\/service `9999562' specified}) - end - end - - it 'contains the rule' do - run_shell('iptables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m multiport --dports 9999562-563 -m comment --comment "562 - test" -j ACCEPT}) - end - end - end - end - describe 'purge tests' do before :all do iptables_flush_all_tables @@ -547,11 +509,13 @@ class { 'firewall': } after(:all) do iptables_flush_all_tables end + before(:each) do iptables_flush_all_tables run_shell('iptables -A INPUT -p tcp -s 1.2.1.1') run_shell('iptables -A INPUT -p udp -s 1.2.1.1') + run_shell('iptables -A INPUT -s 1.2.1.3 -m comment --comment "010 input-1.2.1.3"') run_shell('iptables -A OUTPUT -s 1.2.1.2 -m comment --comment "010 output-1.2.1.2"') end @@ -566,25 +530,28 @@ class { 'firewall': } run_shell('iptables-save') do |r| expect(r.stdout).to match(%r{010 output-1\.2\.1\.2}) - expect(r.stdout).not_to match(%r{1\.2\.1\.1}) - expect(r.stderr).to eq('') + expect(r.stdout).not_to match(%r{1\.2\.1\.(1|3)}) end end - # rubocop:enable RSpec/ExampleLength pp3 = <<-PUPPETCODE class { 'firewall': } - firewallchain { 'OUTPUT:filter:IPv4': + firewallchain { 'INPUT:filter:IPv4': purge => true, } - firewall { '010 output-1.2.1.2': - chain => 'OUTPUT', + firewall { '010 input-1.2.1.3': + chain => 'INPUT', proto => 'all', - source => '1.2.1.2', + source => '1.2.1.3', } PUPPETCODE it 'ignores managed rules' do - apply_manifest(pp3, catch_changes: true) + apply_manifest(pp3, expect_changes: true) + + run_shell('iptables-save') do |r| + expect(r.stdout).not_to match(%r{1\.2\.1\.1}) + expect(r.stdout).to match(%r{010 input-1\.2\.1\.3}) + end end pp4 = <<-PUPPETCODE @@ -592,20 +559,41 @@ class { 'firewall': } firewallchain { 'INPUT:filter:IPv4': purge => true, ignore => [ - '-s 1\.2\.1\.1', + '-s 1.2.1.1', ], } PUPPETCODE it 'ignores specified rules' do - apply_manifest(pp4, catch_changes: true) + apply_manifest(pp4, expect_changes: true) + + run_shell('iptables-save') do |r| + expect(r.stdout).to match(%r{1\.2\.1\.1}) + expect(r.stdout).not_to match(%r{010 input-1\.2\.1\.3}) + end end pp5 = <<-PUPPETCODE + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv4': + purge => true, + ignore_foreign => true, + } + PUPPETCODE + it 'ignores foreign rules' do + apply_manifest(pp5, expect_changes: true) + + run_shell('iptables-save') do |r| + expect(r.stdout).to match(%r{1\.2\.1\.1}) + expect(r.stdout).not_to match(%r{010 input-1\.2\.1\.3}) + end + end + + pp6 = <<-PUPPETCODE class { 'firewall': } firewallchain { 'INPUT:filter:IPv4': purge => true, ignore => [ - '-s 1\.2\.1\.1', + '-s 1.2.1.1', ], } firewall { '014 input-1.2.1.6': @@ -630,9 +618,9 @@ class { 'firewall': } } PUPPETCODE it 'adds managed rules with ignored rules' do - apply_manifest(pp5, catch_failures: true) + apply_manifest(pp6, catch_failures: true) - expect(run_shell('iptables-save').stdout).to match(%r{-A INPUT -s 1\.2\.1\.1(\/32)? -p tcp\s?\n-A INPUT -s 1\.2\.1\.1(\/32)? -p udp}) + expect(run_shell('iptables-save').stdout).to match(%r{-A INPUT -s 1\.2\.1\.1(/32)? -p (tcp|6)\s?\n-A INPUT -s 1\.2\.1\.1(/32)? -p (udp|17)}) end end end @@ -651,18 +639,18 @@ class { '::firewall': } firewall { '560 - test': proto => tcp, sport => '9999560-561', - action => accept, + jump => accept, } PUPPETCODE it 'applies' do apply_manifest(pp19, expect_failures: true) do |r| - expect(r.stderr).to match(%r{invalid port\/service `9999560' specified}) + expect(r.stderr).to match(%r{invalid port/service `9999560' specified}) end end it 'contains the rule' do run_shell('iptables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m multiport --sports 9999560-561 -m comment --comment "560 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m tcp --sport 9999560-561 -m comment --comment "560 - test" -j ACCEPT}) end end end @@ -675,13 +663,13 @@ class { '::firewall': } firewall { '101 test source changes': proto => tcp, dport => '101', - action => accept, + jump => accept, source => '8.0.0.1', } firewall { '100 test source static': proto => tcp, dport => '100', - action => accept, + jump => accept, source => '8.0.0.2', } PUPPETCODE @@ -691,17 +679,18 @@ class { '::firewall': } it 'adds a unmanaged rule without a comment' do run_shell('iptables -A INPUT -t filter -s 8.0.0.3/32 -p tcp -m multiport --dports 102 -j ACCEPT') - expect(run_shell('iptables-save').stdout).to match(%r{-A INPUT -s 8\.0\.0\.3(\/32)? -p tcp -m multiport --dports 102 -j ACCEPT}) + expect(run_shell('iptables-save').stdout).to match(%r{-A INPUT -s 8\.0\.0\.3(/32)? -p (tcp|6) -m multiport --dports 102 -j ACCEPT}) end it 'contains the changable 8.0.0.1 rule' do run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -s 8\.0\.0\.1(\/32)? -p tcp -m multiport --dports 101 -m comment --comment "101 test source changes" -j ACCEPT}) + expect(r.stdout).to match(%r{-A INPUT -s 8\.0\.0\.1(/32)? -p (tcp|6) -m tcp --dport 101 -m comment --comment "101 test source changes" -j ACCEPT}) end end + it 'contains the static 8.0.0.2 rule' do # rubocop:disable RSpec/RepeatedExample : The values being matched differ run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -s 8\.0\.0\.2(\/32)? -p tcp -m multiport --dports 100 -m comment --comment "100 test source static" -j ACCEPT}) + expect(r.stdout).to match(%r{-A INPUT -s 8\.0\.0\.2(/32)? -p (tcp|6) -m tcp --dport 100 -m comment --comment "100 test source static" -j ACCEPT}) end end @@ -710,13 +699,12 @@ class { '::firewall': } firewall { '101 test source changes': proto => tcp, dport => '101', - action => accept, + jump => accept, source => '8.0.0.4', } PUPPETCODE it 'changes to 8.0.0.4 second' do - expect(apply_manifest(pp2, catch_failures: true).stdout) - .to match(%r{Notice: \/Stage\[main\]\/Main\/Firewall\[101 test source changes\]\/source: source changed '8\.0\.0\.1\/32' to '8\.0\.0\.4\/32'}) + apply_manifest(pp2, catch_failures: true) end it 'does not contain the old changing 8.0.0.1 rule' do @@ -724,14 +712,16 @@ class { '::firewall': } expect(r.stdout).not_to match(%r{8\.0\.0\.1}) end end + it 'contains the staic 8.0.0.2 rule' do # rubocop:disable RSpec/RepeatedExample : The values being matched differ run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -s 8\.0\.0\.2(\/32)? -p tcp -m multiport --dports 100 -m comment --comment "100 test source static" -j ACCEPT}) + expect(r.stdout).to match(%r{-A INPUT -s 8\.0\.0\.2(/32)? -p (tcp|6) -m tcp --dport 100 -m comment --comment "100 test source static" -j ACCEPT}) end end + it 'contains the changing new 8.0.0.4 rule' do run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -s 8\.0\.0\.4(\/32)? -p tcp -m multiport --dports 101 -m comment --comment "101 test source changes" -j ACCEPT}) + expect(r.stdout).to match(%r{-A INPUT -s 8\.0\.0\.4(/32)? -p (tcp|6) -m tcp --dport 101 -m comment --comment "101 test source changes" -j ACCEPT}) end end end @@ -739,12 +729,12 @@ class { '::firewall': } ['dst_type', 'src_type'].each do |type| describe type.to_s do - context 'when LOCAL --limit-iface-in', unless: (os[:family] == 'redhat' && os[:release].start_with?('5')) do + context 'when LOCAL --limit-iface-in' do pp97 = <<-PUPPETCODE class { '::firewall': } firewall { '613 - test': proto => tcp, - action => accept, + jump => accept, #{type} => 'LOCAL --limit-iface-in', } PUPPETCODE @@ -754,61 +744,39 @@ class { '::firewall': } it 'contains the rule' do run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "613 - test" -j ACCEPT}) - end - end - end - - context 'when LOCAL --limit-iface-in fail', if: (os[:family] == 'redhat' && os[:release].start_with?('5')) do - pp98 = <<-PUPPETCODE - class { '::firewall': } - firewall { '614 - test': - proto => tcp, - action => accept, - #{type} => 'LOCAL --limit-iface-in', - } - PUPPETCODE - it 'fails' do - apply_manifest(pp98, expect_failures: true) do |r| - expect(r.stderr).to match(%r{--limit-iface-in and --limit-iface-out are available from iptables version}) - end - end - - it 'does not contain the rule' do - run_shell('iptables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "614 - test" -j ACCEPT}) + expect(r.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "613 - test" -j ACCEPT}) end end end - context 'when duplicated LOCAL', unless: (os[:family] == 'redhat' && os[:release].start_with?('5')) do + context 'when duplicated LOCAL' do pp99 = <<-PUPPETCODE class { '::firewall': } firewall { '615 - test': proto => tcp, - action => accept, + jump => accept, #{type} => ['LOCAL', 'LOCAL'], } PUPPETCODE it 'fails' do apply_manifest(pp99, expect_failures: true) do |r| - expect(r.stderr).to match(%r{#{type} elements must be unique}) + expect(r.stderr).to match(%r{`#{type}` elements must be unique}) end end it 'does not contain the rule' do run_shell('iptables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype --#{type.tr('_', '-')} LOCAL -m comment --comment "615 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype --#{type.tr('_', '-')} LOCAL -m comment --comment "615 - test" -j ACCEPT}) end end end - context 'when multiple addrtype', unless: (os[:family] == 'redhat' && os[:release].start_with?('5')) do + context 'when multiple addrtype' do pp100 = <<-PUPPETCODE class { '::firewall': } firewall { '616 - test': proto => tcp, - action => accept, + jump => accept, #{type} => ['LOCAL', '! LOCAL'], } PUPPETCODE @@ -818,73 +786,7 @@ class { '::firewall': } it 'contains the rule' do run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "616 - test" -j ACCEPT}) - end - end - end - - context 'when multiple addrtype fail', if: (os[:family] == 'redhat' && os[:release].start_with?('5')) do - pp101 = <<-PUPPETCODE - class { '::firewall': } - firewall { '616 - test': - proto => tcp, - action => accept, - #{type} => ['LOCAL', '! LOCAL'], - } - PUPPETCODE - it 'fails' do - apply_manifest(pp101, expect_failures: true) do |r| - expect(r.stderr).to match(%r{Multiple #{type} elements are available from iptables version}) - end - end - - it 'does not contain the rule' do - run_shell('iptables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "616 - test" -j ACCEPT}) - end - end - end - - context 'when LOCAL --limit-iface-in', unless: (os[:family] == 'redhat' && os[:release].start_with?('5') - ) do - pp102 = <<-PUPPETCODE - class { '::firewall': } - firewall { '617 - test': - proto => tcp, - action => accept, - #{type} => 'LOCAL --limit-iface-in', - } - PUPPETCODE - it 'applies' do - apply_manifest(pp102, catch_failures: true) - end - - it 'contains the rule' do - run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "617 - test" -j ACCEPT}) - end - end - end - - context 'when LOCAL --limit-iface-in fail', if: (os[:family] == 'redhat' && os[:release].start_with?('5') - ) do - pp103 = <<-PUPPETCODE - class { '::firewall': } - firewall { '618 - test': - proto => tcp, - action => accept, - #{type} => 'LOCAL --limit-iface-in', - } - PUPPETCODE - it 'fails' do - apply_manifest(pp103, expect_failures: true) do |r| - expect(r.stderr).to match(%r{--limit-iface-in and --limit-iface-out are available from iptables version}) - end - end - - it 'does not contain the rule' do - run_shell('iptables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "618 - test" -j ACCEPT}) + expect(r.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "616 - test" -j ACCEPT}) end end end @@ -897,7 +799,7 @@ class { '::firewall': } class { '::firewall': } firewall { '566 - test': proto => tcp, - action => accept, + jump => accept, table => 'mangle', } PUPPETCODE @@ -907,16 +809,17 @@ class { '::firewall': } it 'contains the rule' do run_shell('iptables-save -t mangle') do |r| - expect(r.stdout).to match(%r{-A INPUT -p tcp -m comment --comment "566 - test" -j ACCEPT}) + expect(r.stdout).to match(%r{-A INPUT -p (tcp|6) -m comment --comment "566 - test" -j ACCEPT}) end end end + context 'when nat' do pp32 = <<-PUPPETCODE class { '::firewall': } firewall { '566 - test2': proto => tcp, - action => accept, + jump => accept, table => 'nat', chain => 'OUTPUT', } @@ -927,7 +830,7 @@ class { '::firewall': } it 'does not contain the rule' do run_shell('iptables-save -t nat') do |r| - expect(r.stdout).to match(%r{-A OUTPUT -p tcp -m comment --comment "566 - test2" -j ACCEPT}) + expect(r.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m comment --comment "566 - test2" -j ACCEPT}) end end end @@ -952,7 +855,7 @@ class { '::firewall': } it 'contains the rule' do run_shell('iptables-save -t nat') do |r| - expect(r.stdout).to match(%r{-A PREROUTING -s 200.200.200.200(\/32)? -p tcp -m comment --comment "569 - test" -j NETMAP --to 192.168.1.1}) + expect(r.stdout).to match(%r{-A PREROUTING -s 200.200.200.200(/32)? -p (tcp|6) -m comment --comment "569 - test" -j NETMAP --to 192.168.1.1}) end end end @@ -982,63 +885,61 @@ class { '::firewall': } it 'contains the rule' do run_shell('iptables-save -t nat') do |r| - expect(r.stdout).to match(%r{-A POSTROUTING -d 200.200.200.200(\/32)? -p tcp -m comment --comment "569 - test" -j NETMAP --to 192.168.1.1}) + expect(r.stdout).to match(%r{-A POSTROUTING -d 200.200.200.200(/32)? -p (tcp|6) -m comment --comment "569 - test" -j NETMAP --to 192.168.1.1}) end end end end - unless (os[:family] == 'redhat' && os[:release].start_with?('5', '6')) || (os[:family] == 'sles') - describe 'ipvs' do - context 'when set' do - pp1 = <<-PUPPETCODE - class { '::firewall': } - firewall { '1002 - set ipvs': - proto => 'tcp', - action => accept, - chain => 'INPUT', - ipvs => true, - } - PUPPETCODE - it 'applies' do - apply_manifest(pp1, catch_failures: true) - end + describe 'ipvs' do + context 'when set' do + pp1 = <<-PUPPETCODE + class { '::firewall': } + firewall { '1002 - set ipvs': + proto => 'tcp', + jump => accept, + chain => 'INPUT', + ipvs => true, + } + PUPPETCODE + it 'applies' do + apply_manifest(pp1, catch_failures: true) + end - it 'contains the rule' do - run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -p tcp -m ipvs --ipvs -m comment --comment "1002 - set ipvs" -j ACCEPT}) - end + it 'contains the rule' do + run_shell('iptables-save') do |r| + expect(r.stdout).to match(%r{-A INPUT -p (tcp|6) -m ipvs --ipvs -m comment --comment "1002 - set ipvs" -j ACCEPT}) end end end + end - describe 'tee_gateway' do - context 'when 10.0.0.2' do - pp1 = <<-PUPPETCODE - class { '::firewall': } - firewall { - '810 - tee_gateway': - chain => 'PREROUTING', - table => 'mangle', - jump => 'TEE', - gateway => '10.0.0.2', - proto => all, - } - PUPPETCODE - it 'applies' do - apply_manifest(pp1, catch_failures: true) - end + describe 'tee_gateway' do + context 'when 10.0.0.2' do + pp1 = <<-PUPPETCODE + class { '::firewall': } + firewall { + '810 - tee_gateway': + chain => 'PREROUTING', + table => 'mangle', + jump => 'TEE', + gateway => '10.0.0.2', + proto => all, + } + PUPPETCODE + it 'applies' do + apply_manifest(pp1, catch_failures: true) + end - it 'contains the rule' do - run_shell('iptables-save -t mangle') do |r| - expect(r.stdout).to match(%r{-A PREROUTING -m comment --comment "810 - tee_gateway" -j TEE --gateway 10.0.0.2}) - end + it 'contains the rule' do + run_shell('iptables-save -t mangle') do |r| + expect(r.stdout).to match(%r{-A PREROUTING -m comment --comment "810 - tee_gateway" -j TEE --gateway 10.0.0.2}) end end end end - unless (os[:family] == 'redhat' && os[:release].start_with?('5', '6', '8', '9')) || (os[:family] == 'sles') + unless (os[:family] == 'redhat' && os[:release].start_with?('8', '9')) || (os[:family] == 'sles') describe 'time tests' do context 'when set all time parameters' do pp1 = <<-PUPPETCODE @@ -1046,13 +947,13 @@ class { '::firewall': } firewall { '805 - test': proto => tcp, dport => '8080', - action => accept, + jump => accept, chain => 'OUTPUT', date_start => '2016-01-19T04:17:07', date_stop => '2038-01-19T04:17:07', time_start => '6:00', time_stop => '17:00:00', - month_days => '7', + month_days => 7, week_days => 'Tue', kernel_timezone => true, } @@ -1064,7 +965,7 @@ class { '::firewall': } it 'contains the rule' do run_shell('iptables-save') do |r| expect(r.stdout).to match( - %r{-A OUTPUT -p tcp -m multiport --dports 8080 -m time --timestart 06:00:00 --timestop 17:00:00 --monthdays 7 --weekdays Tue --datestart 2016-01-19T04:17:07 --datestop 2038-01-19T04:17:07 --kerneltz -m comment --comment "805 - test" -j ACCEPT}, # rubocop:disable Layout/LineLength + %r{-A OUTPUT -p (tcp|6) -m tcp --dport 8080 -m time --timestart 06:00:00 --timestop 17:00:00 --monthdays 7 --weekdays Tue --datestart 2016-01-19T04:17:07 --datestop 2038-01-19T04:17:07 --kerneltz -m comment --comment "805 - test" -j ACCEPT}, # rubocop:disable Layout/LineLength ) end end @@ -1072,259 +973,264 @@ class { '::firewall': } end end - unless (os[:family] == 'redhat' && os[:release].start_with?('5')) || os[:family] == 'sles' - describe 'checksum_fill' do - context 'when virbr' do - pp38 = <<-PUPPETCODE - class { '::firewall': } - firewall { '576 - test': - proto => udp, - table => 'mangle', - outiface => 'virbr0', - chain => 'POSTROUTING', - dport => '68', - jump => 'CHECKSUM', - checksum_fill => true, - provider => iptables, - } - PUPPETCODE - it 'applies' do - apply_manifest(pp38, catch_failures: true) + describe 'checksum_fill' do + context 'when virbr' do + pp38 = <<-PUPPETCODE + class { '::firewall': } + firewall { '576 - test': + proto => udp, + table => 'mangle', + outiface => 'virbr0', + chain => 'POSTROUTING', + dport => '68', + jump => 'CHECKSUM', + checksum_fill => true, + protocol => iptables, + } + PUPPETCODE + it 'applies' do + apply_manifest(pp38, catch_failures: true) + end + + it 'contains the rule' do + run_shell('iptables-save -t mangle') do |r| + expect(r.stdout).to match(%r{-A POSTROUTING -o virbr0 -p (udp|17) -m udp --dport 68 -m comment --comment "576 - test" -j CHECKSUM --checksum-fill}) end + end + end + end - it 'contains the rule' do - run_shell('iptables-save -t mangle') do |r| - expect(r.stdout).to match(%r{-A POSTROUTING -o virbr0 -p udp -m multiport --dports 68 -m comment --comment "576 - test" -j CHECKSUM --checksum-fill}) - end + # RHEL5/SLES does not support -m socket + describe 'socket' do + context 'when true' do + pp78 = <<-PUPPETCODE + class { '::firewall': } + firewall { '585 - test': + ensure => present, + proto => tcp, + dport => '585', + jump => accept, + chain => 'PREROUTING', + table => 'nat', + socket => true, + } + PUPPETCODE + it 'applies' do + apply_manifest(pp78, catch_failures: true) + end + + it 'contains the rule' do + run_shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(%r{-A PREROUTING -p (tcp|6) -m tcp --dport 585 -m socket -m comment --comment "585 - test" -j ACCEPT}) end end end - # RHEL5/SLES does not support -m socket - describe 'socket' do - context 'when true' do - pp78 = <<-PUPPETCODE + context 'when false' do + pp79 = <<-PUPPETCODE + class { '::firewall': } + firewall { '586 - test': + ensure => present, + proto => tcp, + dport => '586', + jump => accept, + chain => 'PREROUTING', + table => 'nat', + socket => false, + } + PUPPETCODE + it 'applies' do + apply_manifest(pp79, catch_failures: true) + end + + it 'contains the rule' do + run_shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(%r{-A PREROUTING -p (tcp|6) -m tcp --dport 586 -m comment --comment "586 - test" -j ACCEPT}) + end + end + end + + shared_examples 'is idempotent' do |value, line_match| + pp1 = <<-PUPPETCODE class { '::firewall': } - firewall { '585 - test': + firewall { '598 - test': ensure => present, - proto => tcp, - dport => '585', - action => accept, + proto => 'tcp', chain => 'PREROUTING', - table => 'nat', - socket => true, + table => 'raw', + #{value} } - PUPPETCODE - it 'applies' do - apply_manifest(pp78, catch_failures: true) - end + PUPPETCODE + it "changes the value to #{value}" do + # apply_manifest(pp1, catch_failures: true) + apply_manifest(pp1, expect_changes: true) - it 'contains the rule' do - run_shell('iptables-save -t nat') do |r| - expect(r.stdout).to match(%r{-A PREROUTING -p tcp -m multiport --dports 585 -m socket -m comment --comment "585 - test" -j ACCEPT}) - end + run_shell('iptables-save -t raw') do |r| + expect(r.stdout).to match(%r{#{line_match}}) end end + end - context 'when false' do - pp79 = <<-PUPPETCODE + shared_examples "doesn't change" do |value, line_match| + pp2 = <<-PUPPETCODE class { '::firewall': } - firewall { '586 - test': + firewall { '598 - test': ensure => present, - proto => tcp, - dport => '586', - action => accept, + proto => 'tcp', chain => 'PREROUTING', - table => 'nat', - socket => false, + table => 'raw', + #{value} } - PUPPETCODE - it 'applies' do - apply_manifest(pp79, catch_failures: true) - end + PUPPETCODE + it "doesn't change the value to #{value}" do + apply_manifest(pp2) - it 'contains the rule' do - run_shell('iptables-save -t nat') do |r| - expect(r.stdout).to match(%r{-A PREROUTING -p tcp -m multiport --dports 586 -m comment --comment "586 - test" -j ACCEPT}) - end + run_shell('iptables-save -t raw') do |r| + expect(r.stdout).to match(%r{#{line_match}}) end end + end - shared_examples 'is idempotent' do |value, line_match| - pp1 = <<-PUPPETCODE - class { '::firewall': } - firewall { '598 - test': - ensure => present, - proto => 'tcp', - chain => 'PREROUTING', - table => 'raw', - #{value} - } - PUPPETCODE - it "changes the value to #{value}" do - # apply_manifest(pp1, catch_failures: true) - apply_manifest(pp1, expect_changes: true) - - run_shell('iptables-save -t raw') do |r| - expect(r.stdout).to match(%r{#{line_match}}) - end + describe 'adding a rule' do + context 'when unset' do + before :all do + iptables_flush_all_tables end - end - shared_examples "doesn't change" do |value, line_match| - pp2 = <<-PUPPETCODE - class { '::firewall': } - firewall { '598 - test': - ensure => present, - proto => 'tcp', - chain => 'PREROUTING', - table => 'raw', - #{value} - } - PUPPETCODE - it "doesn't change the value to #{value}" do - apply_manifest(pp2) + it_behaves_like 'is idempotent', '', %r{-A PREROUTING -p (tcp|6) -m comment --comment "598 - test"} + end - run_shell('iptables-save -t raw') do |r| - expect(r.stdout).to match(%r{#{line_match}}) - end + context 'when set to true' do + before :all do + iptables_flush_all_tables end + + it_behaves_like 'is idempotent', 'socket => true,', %r{-A PREROUTING -p (tcp|6) -m socket -m comment --comment "598 - test"} end - describe 'adding a rule' do - context 'when unset' do - before :all do - iptables_flush_all_tables - end - it_behaves_like 'is idempotent', '', %r{-A PREROUTING -p tcp -m comment --comment "598 - test"} - end - context 'when set to true' do - before :all do - iptables_flush_all_tables - end - it_behaves_like 'is idempotent', 'socket => true,', %r{-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"} - end - context 'when set to false' do - before :all do - iptables_flush_all_tables - end - it_behaves_like 'is idempotent', 'socket => false,', %r{-A PREROUTING -p tcp -m comment --comment "598 - test"} + context 'when set to false' do + before :all do + iptables_flush_all_tables end + + it_behaves_like 'is idempotent', 'socket => false,', %r{-A PREROUTING -p (tcp|6) -m comment --comment "598 - test"} end + end - describe 'editing a rule' do - context 'when unset or false and current value is false' do - before :each do - iptables_flush_all_tables - run_shell('iptables -t raw -A PREROUTING -p tcp -m comment --comment "598 - test"') - end - it_behaves_like "doesn't change", 'socket => false,', %r{-A PREROUTING -p tcp -m comment --comment "598 - test"} + describe 'editing a rule' do + context 'when unset or false and current value is false' do + before :each do + iptables_flush_all_tables + run_shell('iptables -t raw -A PREROUTING -p tcp -m comment --comment "598 - test"') end - context 'when unset or false and current value is true' do - before :each do - iptables_flush_all_tables - run_shell('iptables -t raw -A PREROUTING -p tcp -m comment --comment "598 - test"') - end - it_behaves_like 'is idempotent', 'socket => true,', %r{-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"} - end - context 'when set to true and current value is false' do - before :each do - iptables_flush_all_tables - run_shell('iptables -t raw -A PREROUTING -p tcp -m socket -m comment --comment "598 - test"') - end - it_behaves_like 'is idempotent', 'socket => false,', %r{-A PREROUTING -p tcp -m comment --comment "598 - test"} - end - context 'when set to true and current value is true' do - before :each do - iptables_flush_all_tables - run_shell('iptables -t raw -A PREROUTING -p tcp -m socket -m comment --comment "598 - test"') - end - it_behaves_like "doesn't change", 'socket => true,', %r{-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"} - end + it_behaves_like "doesn't change", 'socket => false,', %r{-A PREROUTING -p (tcp|6) -m comment --comment "598 - test"} end - end - end - # RHEL5 does not support --random - unless os[:family] == 'redhat' && os[:release].start_with?('5') + context 'when unset or false and current value is true' do + before :each do + iptables_flush_all_tables + run_shell('iptables -t raw -A PREROUTING -p tcp -m comment --comment "598 - test"') + end - describe 'match_mark' do - context 'when 0x1' do - pp1 = <<-PUPPETCODE - class { '::firewall': } - firewall { '503 match_mark - test': - proto => 'all', - match_mark => '0x1', - action => reject, - } - PUPPETCODE - it 'applies' do - apply_manifest(pp1, catch_failures: true) + it_behaves_like 'is idempotent', 'socket => true,', %r{-A PREROUTING -p (tcp|6) -m socket -m comment --comment "598 - test"} + end + + context 'when set to true and current value is false' do + before :each do + iptables_flush_all_tables + run_shell('iptables -t raw -A PREROUTING -p tcp -m socket -m comment --comment "598 - test"') end - it 'contains the rule' do - run_shell('iptables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -m mark --mark 0x1 -m comment --comment "503 match_mark - test" -j REJECT --reject-with icmp-port-unreachable}) - end + it_behaves_like 'is idempotent', 'socket => false,', %r{-A PREROUTING -p (tcp|6) -m comment --comment "598 - test"} + end + + context 'when set to true and current value is true' do + before :each do + iptables_flush_all_tables + run_shell('iptables -t raw -A PREROUTING -p tcp -m socket -m comment --comment "598 - test"') end + + it_behaves_like "doesn't change", 'socket => true,', %r{-A PREROUTING -p (tcp|6) -m socket -m comment --comment "598 - test"} end end + end - # iptables version 1.3.5 does not support masks on MARK rules - describe 'set_mark' do - context 'when 0x3e8/0xffffffff' do - pp73 = <<-PUPPETCODE + describe 'match_mark' do + context 'when 0x1' do + pp1 = <<-PUPPETCODE class { '::firewall': } - firewall { '580 - test': - ensure => present, - chain => 'OUTPUT', - proto => tcp, - dport => '580', - jump => 'MARK', - table => 'mangle', - set_mark => '0x3e8/0xffffffff', + firewall { '503 match_mark - test': + proto => 'all', + match_mark => '0x1', + jump => reject, } - PUPPETCODE - it 'applies' do - apply_manifest(pp73, catch_failures: true) - end + PUPPETCODE + it 'applies' do + apply_manifest(pp1, catch_failures: true) + end - it 'contains the rule' do - run_shell('iptables-save -t mangle') do |r| - expect(r.stdout).to match(%r{-A OUTPUT -p tcp -m multiport --dports 580 -m comment --comment "580 - test" -j MARK --set-xmark 0x3e8\/0xffffffff}) - end + it 'contains the rule' do + run_shell('iptables-save') do |r| + expect(r.stdout).to match(%r{-A INPUT -m mark --mark 0x1 -m comment --comment "503 match_mark - test" -j REJECT --reject-with icmp-port-unreachable}) end end end + end - describe 'random' do - context 'when 192.168.1.1' do - pp40 = <<-PUPPETCODE - class { '::firewall': } - firewall { '570 - random': - proto => all, - table => 'nat', - chain => 'POSTROUTING', - jump => 'MASQUERADE', - source => '172.30.0.0/16', - random => true - } - PUPPETCODE - it 'applies manifest twice' do - idempotent_apply(pp40) + # iptables version 1.3.5 does not support masks on MARK rules + describe 'set_mark' do + context 'when 0x3e8/0xffffffff' do + pp73 = <<-PUPPETCODE + class { '::firewall': } + firewall { '580 - test': + ensure => present, + chain => 'OUTPUT', + proto => tcp, + dport => '580', + jump => 'MARK', + table => 'mangle', + set_mark => '0x3e8/0xffffffff', + } + PUPPETCODE + it 'applies' do + apply_manifest(pp73, catch_failures: true) + end + + it 'contains the rule' do + run_shell('iptables-save -t mangle') do |r| + expect(r.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m tcp --dport 580 -m comment --comment "580 - test" -j MARK --set-xmark 0x3e8/0xffffffff}) end + end + end + end - it 'contains the rule' do - run_shell('iptables-save -t nat') do |r| - expect(r.stdout).to match(%r{-A POSTROUTING -s 172\.30\.0\.0\/16 -m comment --comment "570 - random" -j MASQUERADE --random}) - end + describe 'random' do + context 'when 192.168.1.1' do + pp40 = <<-PUPPETCODE + class { '::firewall': } + firewall { '570 - random': + proto => all, + table => 'nat', + chain => 'POSTROUTING', + jump => 'MASQUERADE', + source => '172.30.0.0/16', + random => true + } + PUPPETCODE + it 'applies manifest twice' do + idempotent_apply(pp40) + end + + it 'contains the rule' do + run_shell('iptables-save -t nat') do |r| + expect(r.stdout).to match(%r{-A POSTROUTING -s 172\.30\.0\.0/16 -m comment --comment "570 - random" -j MASQUERADE --random}) end end end end - describe 'hashlimit', unless: ((os[:family] == 'redhat' && os[:release][0] <= '5')) do + describe 'hashlimit' do before(:all) do pp = <<-PUPPETCODE firewall { '805 - hashlimit_above test': @@ -1332,19 +1238,19 @@ class { '::firewall': } proto => 'tcp', hashlimit_name => 'above', hashlimit_above => '526/sec', - hashlimit_htable_gcinterval => '10', + hashlimit_htable_gcinterval => 10, hashlimit_mode => 'srcip,dstip', - action => accept, + jump => accept, } firewall { '806 - hashlimit_upto test': chain => 'INPUT', hashlimit_name => 'upto', hashlimit_upto => '16/sec', - hashlimit_burst => '640', - hashlimit_htable_size => '1000000', - hashlimit_htable_max => '320000', - hashlimit_htable_expire => '36000000', - action => accept, + hashlimit_burst => 640, + hashlimit_htable_size => 1000000, + hashlimit_htable_max => 320000, + hashlimit_htable_expire => 36000000, + jump => accept, } PUPPETCODE idempotent_apply(pp) @@ -1353,26 +1259,19 @@ class { '::firewall': } let(:result) { run_shell('iptables-save') } it 'hashlimit_above is set' do - regex_array = [%r{-A INPUT}, %r{-p tcp}, %r{--hashlimit-above 526\/sec}, %r{--hashlimit-mode srcip,dstip}, %r{--hashlimit-name above}, %r{--hashlimit-htable-gcinterval 10}, %r{-j ACCEPT}] + regex_array = [%r{-A INPUT}, %r{-p (tcp|6)}, %r{--hashlimit-above 526/sec}, %r{--hashlimit-mode srcip,dstip}, %r{--hashlimit-name above}, %r{--hashlimit-htable-gcinterval 10}, %r{-j ACCEPT}] regex_array.each do |regex| expect(result.stdout).to match(regex) end end + it 'hashlimit_upto is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m hashlimit --hashlimit-upto 16\/sec --hashlimit-burst 640 --hashlimit-name upto --hashlimit-htable-size 1000000 --hashlimit-htable-max 320000 --hashlimit-htable-expire 36000000 -m comment --comment "806 - hashlimit_upto test" -j ACCEPT}) # rubocop:disable Layout/LineLength : Cannot reduce line to required length + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m hashlimit --hashlimit-upto 16/sec --hashlimit-burst 640 --hashlimit-name upto --hashlimit-htable-size 1000000 --hashlimit-htable-max 320000 --hashlimit-htable-expire 36000000 -m comment --comment "806 - hashlimit_upto test" -j ACCEPT}) # rubocop:disable Layout/LineLength : Cannot reduce line to required length end end - describe 'random-fully' do - supports_random_fully = if os[:family] == 'redhat' && os[:release].start_with?('8', '9') - true - elsif os[:family] == 'debian' && os[:release].start_with?('10') - true - else - false - end - + describe 'random-fully', if: (os[:family] == 'redhat' && os[:release].start_with?('8', '9')) || (os[:family] == 'debian' && os[:release].start_with?('10', '11')) do before(:all) do pp = <<-PUPPETCODE firewall { '901 - set random-fully': @@ -1387,11 +1286,8 @@ class { '::firewall': } let(:result) { run_shell('iptables-save') } - it 'adds random-fully rule', if: supports_random_fully do - expect(result.stdout).to match(%r{-A POSTROUTING -p tcp -m comment --comment "901 - set random-fully" -j MASQUERADE --random-fully}) - end - it 'adds rule without random-fully', unless: supports_random_fully do - expect(result.stdout).to match(%r{-A POSTROUTING -p tcp -m comment --comment "901 - set random-fully" -j MASQUERADE}) + it 'adds random-fully rule' do + expect(result.stdout).to match(%r{-A POSTROUTING -p (tcp|6) -m comment --comment "901 - set random-fully" -j MASQUERADE --random-fully}) end end @@ -1405,13 +1301,14 @@ class { '::firewall': } chain => 'INPUT', iniface => 'enp0s8', proto => 'icmp', - action => 'drop', + jump => 'drop', } } PUPPETCODE it 'applies' do apply_manifest(pp) end + if fetch_os_name == 'ubuntu' it 'contains the rule' do run_shell('iptables-save') do |r| diff --git a/spec/acceptance/firewall_attributes_happy_path_spec.rb b/spec/acceptance/firewall_attributes_happy_path_spec.rb index a1b73022d..43e5ca291 100644 --- a/spec/acceptance/firewall_attributes_happy_path_spec.rb +++ b/spec/acceptance/firewall_attributes_happy_path_spec.rb @@ -4,18 +4,13 @@ describe 'firewall attribute testing, happy path' do before :all do - if os[:family] == 'redhat' - pre_setup - end + pre_setup if os[:family] == 'redhat' iptables_flush_all_tables ip6tables_flush_all_tables end describe 'attributes test' do before(:all) do - notrack_manifest = "jump => 'CT', notrack => true" - notrack_manifest = "jump => 'NOTRACK'" if os[:family] == 'redhat' && [5, 6].include?(os[:release].to_i) - pp = <<-PUPPETCODE class { '::firewall': } firewall { '004 - log_level and log_prefix': @@ -26,97 +21,97 @@ class { '::firewall': } log_level => '3', log_prefix => 'IPTABLES dropped invalid: ', } - # firewall { '501 - connlimit': - # proto => tcp, - # dport => '2222', - # connlimit_above => '10', - # connlimit_mask => '24', - # action => reject, - # } + firewall { '501 - connlimit': + proto => tcp, + dport => '2222', + connlimit_above => 10, + connlimit_mask => 24, + jump => reject, + } firewall { '502 - connmark': proto => 'all', connmark => '0x1', - action => reject, + jump => reject, } firewall { '550 - destination': - proto => tcp, - dport => '550', - action => accept, + proto => tcp, + dport => '550', + jump => accept, destination => '192.168.2.0/24', } firewall { '551 - destination negated': proto => tcp, - dport => '551', - action => accept, + dport => '551', + jump => accept, destination => '! 192.168.2.0/24', } firewall { '556 - source': proto => tcp, - dport => '556', - action => accept, + dport => '556', + jump => accept, source => '192.168.2.0/24', } firewall { '557 - source negated': proto => tcp, - dport => '557', - action => accept, + dport => '557', + jump => accept, source => '! 192.168.2.0/24', } firewall { '558 - src_range': proto => tcp, - dport => '558', - action => accept, + dport => '558', + jump => accept, src_range => '192.168.1.1-192.168.1.10', } firewall { '559 - dst_range': proto => tcp, - dport => '559', - action => accept, + dport => '559', + jump => accept, dst_range => '192.168.1.1-192.168.1.10', } firewall { '560 - sport range': proto => tcp, - sport => '560-561', - action => accept, + sport => '560:561', + jump => accept, } firewall { '561 - dport range': proto => tcp, - dport => '561-562', - action => accept, + dport => '561:562', + jump => accept, } firewall { '563 - dst_type': proto => tcp, - action => accept, + jump => accept, dst_type => 'MULTICAST', } firewall { '564 - src_type negated': proto => tcp, - action => accept, + jump => accept, src_type => '! MULTICAST', } firewall { '565 - tcp_flags': proto => tcp, - action => accept, + jump => accept, tcp_flags => 'FIN,SYN ACK', } firewall { '566 - chain': proto => tcp, - action => accept, + jump => accept, chain => 'FORWARD', } firewallchain { 'TEST:filter:IPv4': - ensure => present, + ensure => present, } firewall { '567 - jump': proto => tcp, chain => 'INPUT', - jump => 'TEST', + jump => 'TEST', } firewall { '568 - tosource': - proto => tcp, - table => 'nat', - chain => 'POSTROUTING', - jump => 'SNAT', + proto => tcp, + table => 'nat', + chain => 'POSTROUTING', + jump => 'SNAT', tosource => '192.168.1.1', } firewall { '569 - todest': @@ -124,48 +119,48 @@ class { '::firewall': } table => 'nat', chain => 'PREROUTING', jump => 'DNAT', - source => '200.200.200.200', + source => '200.200.200.200/32', todest => '192.168.1.1', } firewall { '572 - limit': ensure => present, - proto => tcp, - dport => '572', - action => accept, - limit => '500/sec', + proto => tcp, + dport => '572', + jump => accept, + limit => '500/sec', } firewall { '573 - burst': ensure => present, - proto => tcp, - dport => '573', - action => accept, - limit => '500/sec', - burst => '1500', + proto => tcp, + dport => '573', + jump => accept, + limit => '500/sec', + burst => 1500, } firewall { '574 - toports': - proto => icmp, - table => 'nat', - chain => 'PREROUTING', - jump => 'REDIRECT', + proto => icmp, + table => 'nat', + chain => 'PREROUTING', + jump => 'REDIRECT', toports => '2222', } firewall { '581 - pkttype': - ensure => present, - proto => tcp, + ensure => present, + proto => tcp, dport => '581', - action => accept, + jump => accept, pkttype => 'multicast', } firewall { '583 - isfragment': - ensure => present, - proto => tcp, - dport => '583', - action => accept, + ensure => present, + proto => tcp, + dport => '583', + jump => accept, isfragment => true, } firewall { '595 - ipsec_policy ipsec and out': ensure => 'present', - action => 'reject', + jump => 'reject', chain => 'OUTPUT', destination => '20.0.0.0/8', ipsec_dir => 'out', @@ -176,7 +171,7 @@ class { '::firewall': } } firewall { '596 - ipsec_policy none and in': ensure => 'present', - action => 'reject', + jump => 'reject', chain => 'INPUT', destination => '20.0.0.0/8', ipsec_dir => 'in', @@ -232,14 +227,14 @@ class { '::firewall': } clamp_mss_to_pmtu => true, } firewall { '603 - disallow esp protocol': - action => 'accept', + jump => 'accept', proto => '! esp', } firewall { '604 - set_mss': proto => 'tcp', tcp_flags => 'SYN,RST SYN', jump => 'TCPMSS', - set_mss => '1360', + set_mss => 1360, mss => '1361:1541', chain => 'FORWARD', table => 'mangle', @@ -257,57 +252,57 @@ class { '::firewall': } log_ip_options => true, } firewall { '711 - physdev_in': - chain => 'FORWARD', - proto => tcp, - dport => '711', - action => accept, + chain => 'FORWARD', + proto => tcp, + dport => '711', + jump => accept, physdev_in => 'eth0', } firewall { '712 - physdev_out': - chain => 'FORWARD', - proto => tcp, - dport => '712', - action => accept, + chain => 'FORWARD', + proto => tcp, + dport => '712', + jump => accept, physdev_out => 'eth1', } firewall { '713 - physdev_in physdev_out physdev_is_bridged': - chain => 'FORWARD', - proto => tcp, - dport => '713', - action => accept, - physdev_in => 'eth0', - physdev_out => 'eth1', + chain => 'FORWARD', + proto => tcp, + dport => '713', + jump => accept, + physdev_in => 'eth0', + physdev_out => 'eth1', physdev_is_bridged => true, } firewall { '801 - gid root': chain => 'OUTPUT', - action => accept, - gid => 'root', + jump => accept, + gid => 'root', proto => 'all', } firewall { '802 - gid root negated': chain => 'OUTPUT', - action => accept, - gid => '!root', + jump => accept, + gid => '! root', proto => 'all', } firewall { '803 - uid 0': chain => 'OUTPUT', - action => accept, - uid => '0', + jump => accept, + uid => '0', proto => 'all', } firewall { '804 - uid 0 negated': chain => 'OUTPUT', - action => accept, - uid => '!0', + jump => accept, + uid => '! 0', proto => 'all', } firewall { '807 - ipt_modules tests': proto => tcp, dport => '8080', - action => reject, + jump => reject, chain => 'OUTPUT', uid => 0, gid => 404, @@ -322,7 +317,7 @@ class { '::firewall': } firewall { '808 - ipt_modules tests': proto => tcp, dport => '8080', - action => reject, + jump => reject, chain => 'OUTPUT', gid => 404, dst_range => "100.0.0.1-100.0.0.2", @@ -333,37 +328,38 @@ class { '::firewall': } firewall { '900 - set rpfilter': table => 'raw', chain => 'PREROUTING', - action => 'accept', + jump => 'accept', rpfilter => [ 'invert', 'validmark', 'loose', 'accept-local' ], } firewall { '901 - set rpfilter': table => 'raw', chain => 'PREROUTING', - action => 'accept', + jump => 'accept', rpfilter => 'invert', } firewall { '1000 - set_dscp': proto => 'tcp', jump => 'DSCP', set_dscp => '0x01', - dport => '997', + dport => '997', chain => 'OUTPUT', table => 'mangle', } firewall { '1001 EF - set_dscp_class': proto => 'tcp', jump => 'DSCP', - dport => '997', - set_dscp_class => 'EF', + dport => '997', + set_dscp_class => 'ef', chain => 'OUTPUT', table => 'mangle', } firewall { '004 do not track UDP connections to port 53': - chain => 'PREROUTING', - table => 'raw', - proto => 'udp', - dport => 53, - #{notrack_manifest} + chain => 'PREROUTING', + table => 'raw', + proto => 'udp', + dport => '53', + jump => 'CT', + notrack => true, } PUPPETCODE idempotent_apply(pp) @@ -372,152 +368,193 @@ class { '::firewall': } let(:result) { run_shell('iptables-save') } it 'log_level and log_prefix' do - expect(result.stdout).to match(%r{A INPUT -m conntrack --ctstate INVALID -m comment --comment "004 - log_level and log_prefix" -j LOG --log-prefix "IPTABLES dropped invalid: " --log-level 3}) + expect(result.stdout).to match(%r{-A INPUT -m conntrack --ctstate INVALID -m comment --comment "004 - log_level and log_prefix" -j LOG --log-prefix "IPTABLES dropped invalid: " --log-level 3}) + end + + it 'contains the connlimit and connlimit_mask rule' do + expect(result.stdout).to match( + %r{-A INPUT -p (tcp|6) -m tcp --dport 2222 -m connlimit --connlimit-above 10 --connlimit-mask 24 (--connlimit-saddr )?-m comment --comment "501 - connlimit" -j REJECT --reject-with icmp-port-unreachable}, # rubocop:disable Layout/LineLength + ) end - # it 'contains the connlimit and connlimit_mask rule' do - # expect(result.stdout).to match( - # %r{-A INPUT -p tcp -m multiport --dports 2222 -m connlimit --connlimit-above 10 --connlimit-mask 24 (--connlimit-saddr )?-m comment --comment "501 - connlimit" -j REJECT --reject-with icmp-port-unreachable}, # rubocop:disable Layout/LineLength - # ) - # end + it 'contains connmark' do expect(result.stdout).to match(%r{-A INPUT -m connmark --mark 0x1 -m comment --comment "502 - connmark" -j REJECT --reject-with icmp-port-unreachable}) end + it 'destination is set' do - expect(result.stdout).to match(%r{-A INPUT -d 192.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --dports 550 -m comment --comment "550 - destination" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -d 192.168.2.0/(24|255\.255\.255\.0) -p (tcp|6) -m tcp --dport 550 -m comment --comment "550 - destination" -j ACCEPT}) end + it 'destination is negated' do - expect(result.stdout).to match(%r{-A INPUT (! -d|-d !) 192.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --dports 551 -m comment --comment "551 - destination negated" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT (! -d|-d !) 192.168.2.0/(24|255\.255\.255\.0) -p (tcp|6) -m tcp --dport 551 -m comment --comment "551 - destination negated" -j ACCEPT}) end + it 'source is set' do - expect(result.stdout).to match(%r{-A INPUT -s 192.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --dports 556 -m comment --comment "556 - source" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -s 192.168.2.0/(24|255\.255\.255\.0) -p (tcp|6) -m tcp --dport 556 -m comment --comment "556 - source" -j ACCEPT}) end + it 'source is negated' do - expect(result.stdout).to match(%r{-A INPUT (! -s|-s !) 192.168.2.0\/(24|255\.255\.255\.0) -p tcp -m multiport --dports 557 -m comment --comment "557 - source negated" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT (! -s|-s !) 192.168.2.0/(24|255\.255\.255\.0) -p (tcp|6) -m tcp --dport 557 -m comment --comment "557 - source negated" -j ACCEPT}) end + it 'src_range is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m iprange --src-range 192.168.1.1-192.168.1.10 -m multiport --dports 558 -m comment --comment "558 - src_range" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m iprange --src-range 192.168.1.1-192.168.1.10 -m tcp --dport 558 -m comment --comment "558 - src_range" -j ACCEPT}) end + it 'dst_range is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m iprange --dst-range 192.168.1.1-192.168.1.10 -m multiport --dports 559 -m comment --comment "559 - dst_range" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m iprange --dst-range 192.168.1.1-192.168.1.10 -m tcp --dport 559 -m comment --comment "559 - dst_range" -j ACCEPT}) end + it 'sport range is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --sports 560:561 -m comment --comment "560 - sport range" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --sport 560:561 -m comment --comment "560 - sport range" -j ACCEPT}) end + it 'dport range is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 561:562 -m comment --comment "561 - dport range" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 561:562 -m comment --comment "561 - dport range" -j ACCEPT}) end + it 'dst_type is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m addrtype --dst-type MULTICAST -m comment --comment "563 - dst_type" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype --dst-type MULTICAST -m comment --comment "563 - dst_type" -j ACCEPT}) end + it 'src_type is negated' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m addrtype (! --src-type|--src-type !) MULTICAST -m comment --comment "564 - src_type negated" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype (! --src-type|--src-type !) MULTICAST -m comment --comment "564 - src_type negated" -j ACCEPT}) end + it 'tcp_flags is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN ACK -m comment --comment "565 - tcp_flags" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --tcp-flags FIN,SYN ACK -m comment --comment "565 - tcp_flags" -j ACCEPT}) end + it 'chain is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m comment --comment "566 - chain" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m comment --comment "566 - chain" -j ACCEPT}) end + it 'tosource is set' do - expect(result.stdout).to match(%r{A POSTROUTING -p tcp -m comment --comment "568 - tosource" -j SNAT --to-source 192.168.1.1}) + expect(result.stdout).to match(%r{A POSTROUTING -p (tcp|6) -m comment --comment "568 - tosource" -j SNAT --to-source 192.168.1.1}) end + it 'todest is set' do - expect(result.stdout).to match(%r{-A PREROUTING -s 200.200.200.200(\/32)? -p tcp -m comment --comment "569 - todest" -j DNAT --to-destination 192.168.1.1}) + expect(result.stdout).to match(%r{-A PREROUTING -s 200.200.200.200(/32)? -p (tcp|6) -m comment --comment "569 - todest" -j DNAT --to-destination 192.168.1.1}) end + it 'toports is set' do - expect(result.stdout).to match(%r{-A PREROUTING -p icmp -m comment --comment "574 - toports" -j REDIRECT --to-ports 2222}) + expect(result.stdout).to match(%r{-A PREROUTING -p (icmp|1) -m comment --comment "574 - toports" -j REDIRECT --to-ports 2222}) end + it 'rpfilter is set' do - expect(result.stdout).to match(%r{-A PREROUTING -p tcp -m rpfilter --loose --validmark --accept-local --invert -m comment --comment "900 - set rpfilter" -j ACCEPT}) + expect(result.stdout).to match(%r{-A PREROUTING -p (tcp|6) -m rpfilter --loose --validmark --accept-local --invert -m comment --comment "900 - set rpfilter" -j ACCEPT}) end + it 'single rpfilter is set' do - expect(result.stdout).to match(%r{-A PREROUTING -p tcp -m rpfilter --invert -m comment --comment "901 - set rpfilter" -j ACCEPT}) + expect(result.stdout).to match(%r{-A PREROUTING -p (tcp|6) -m rpfilter --invert -m comment --comment "901 - set rpfilter" -j ACCEPT}) end + it 'limit is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 572 -m limit --limit 500\/sec -m comment --comment "572 - limit" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 572 -m limit --limit 500/sec -m comment --comment "572 - limit" -j ACCEPT}) end + it 'burst is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 573 -m limit --limit 500\/sec --limit-burst 1500 -m comment --comment "573 - burst" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 573 -m limit --limit 500/sec --limit-burst 1500 -m comment --comment "573 - burst" -j ACCEPT}) end + it 'pkttype is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 581 -m pkttype --pkt-type multicast -m comment --comment "581 - pkttype" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 581 -m pkttype --pkt-type multicast -m comment --comment "581 - pkttype" -j ACCEPT}) end + it 'isfragment is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -f -m multiport --dports 583 -m comment --comment "583 - isfragment" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -f -m tcp --dport 583 -m comment --comment "583 - isfragment" -j ACCEPT}) end + it 'ipsec_policy ipsec and dir out' do - expect(result.stdout).to match(%r{-A OUTPUT -d 20.0.0.0\/(8|255\.0\.0\.0) -m policy --dir out --pol ipsec -m comment --comment "595 - ipsec_policy ipsec and out" -j REJECT --reject-with icmp-net-unreachable}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A OUTPUT -d 20.0.0.0/(8|255\.0\.0\.0) -m policy --dir out --pol ipsec -m comment --comment "595 - ipsec_policy ipsec and out" -j REJECT --reject-with icmp-net-unreachable}) # rubocop:disable Layout/LineLength end + it 'ipsec_policy none and dir in' do - expect(result.stdout).to match(%r{-A INPUT -d 20.0.0.0\/(8|255\.0\.0\.0) -m policy --dir in --pol none -m comment --comment "596 - ipsec_policy none and in" -j REJECT --reject-with icmp-net-unreachable}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A INPUT -d 20.0.0.0/(8|255\.0\.0\.0) -m policy --dir in --pol none -m comment --comment "596 - ipsec_policy none and in" -j REJECT --reject-with icmp-net-unreachable}) # rubocop:disable Layout/LineLength end + it 'set_mss is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1541 -m comment --comment "604 - set_mss" -j TCPMSS --set-mss 1360}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1541 -m comment --comment "604 - set_mss" -j TCPMSS --set-mss 1360}) end + it 'clamp_mss_to_pmtu is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -m comment --comment "601 - clamp_mss_to_pmtu" -j TCPMSS --clamp-mss-to-pmtu}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m tcp --tcp-flags SYN,RST SYN -m comment --comment "601 - clamp_mss_to_pmtu" -j TCPMSS --clamp-mss-to-pmtu}) end + it 'comment containing "-A "' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m comment --comment "700 - blah-A Test Rule" -j LOG --log-prefix "FW-A-INPUT: "}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m comment --comment "700 - blah-A Test Rule" -j LOG --log-prefix "FW-A-INPUT: "}) end + it 'set log_uid, log_tcp_sequence, log_tcp_options, log_ip_options' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m comment --comment "701 - log_uid, tcp-sequences and options" -j LOG --log-tcp-sequence --log-tcp-options --log-ip-options --log-uid}) + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m comment --comment "701 - log_uid, tcp-sequences and options" -j LOG --log-tcp-sequence --log-tcp-options --log-ip-options --log-uid}) end + it 'set physdev_in' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-in eth0 -m multiport --dports 711 -m comment --comment "711 - physdev_in" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-in eth0 -m tcp --dport 711 -m comment --comment "711 - physdev_in" -j ACCEPT}) end + it 'set physdev_out' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-out eth1 -m multiport --dports 712 -m comment --comment "712 - physdev_out" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-out eth1 -m tcp --dport 712 -m comment --comment "712 - physdev_out" -j ACCEPT}) end + it 'physdev_in eth0 and physdev_out eth1 and physdev_is_bridged' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-in eth0 --physdev-out eth1 --physdev-is-bridged -m multiport --dports 713 -m comment --comment "713 - physdev_in physdev_out physdev_is_bridged" -j ACCEPT}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-in eth0 --physdev-out eth1 --physdev-is-bridged -m tcp --dport 713 -m comment --comment "713 - physdev_in physdev_out physdev_is_bridged" -j ACCEPT}) # rubocop:disable Layout/LineLength end + it 'gid set to root' do expect(result.stdout).to match(%r{-A OUTPUT -m owner --gid-owner (0|root) -m comment --comment "801 - gid root" -j ACCEPT}) end + it 'gid set to root negated' do expect(result.stdout).to match(%r{-A OUTPUT -m owner ! --gid-owner (0|root) -m comment --comment "802 - gid root negated" -j ACCEPT}) end + it 'uid set to 0' do expect(result.stdout).to match(%r{-A OUTPUT -m owner --uid-owner (0|root) -m comment --comment "803 - uid 0" -j ACCEPT}) end + it 'uid set to 0 negated' do expect(result.stdout).to match(%r{-A OUTPUT -m owner ! --uid-owner (0|root) -m comment --comment "804 - uid 0 negated" -j ACCEPT}) end + it 'set_dscp is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m multiport --dports 997 -m comment --comment "1000 - set_dscp" -j DSCP --set-dscp 0x01}) + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m tcp --dport 997 -m comment --comment "1000 - set_dscp" -j DSCP --set-dscp 0x01}) end + it 'set_dscp_class is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m multiport --dports 997 -m comment --comment "1001 EF - set_dscp_class" -j DSCP --set-dscp 0x2e}) + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m tcp --dport 997 -m comment --comment "1001 EF - set_dscp_class" -j DSCP --set-dscp 0x2e}) end + it 'all the modules with multiple args is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m physdev\s+--physdev-in eth0 --physdev-out eth1 --physdev-is-bridged -m iprange --src-range 90.0.0.1-90.0.0.2\s+--dst-range 100.0.0.1-100.0.0.2 -m owner --uid-owner (0|root) --gid-owner 404 -m multiport --dports 8080 -m addrtype --src-type LOCAL --dst-type UNICAST -m comment --comment "807 - ipt_modules tests" -j REJECT --reject-with icmp-port-unreachable}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m physdev\s+--physdev-in eth0 --physdev-out eth1 --physdev-is-bridged -m iprange --src-range 90.0.0.1-90.0.0.2\s+--dst-range 100.0.0.1-100.0.0.2 -m owner --uid-owner (0|root) --gid-owner 404 -m tcp --dport 8080 -m addrtype --src-type LOCAL -m addrtype --dst-type UNICAST -m comment --comment "807 - ipt_modules tests" -j REJECT --reject-with icmp-port-unreachable}) # rubocop:disable Layout/LineLength end + it 'all the modules with single args is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m physdev\s+--physdev-out eth1 --physdev-is-bridged -m iprange --dst-range 100.0.0.1-100.0.0.2 -m owner --gid-owner 404 -m multiport --dports 8080 -m addrtype --dst-type UNICAST -m comment --comment "808 - ipt_modules tests" -j REJECT --reject-with icmp-port-unreachable}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m physdev\s+--physdev-out eth1 --physdev-is-bridged -m iprange --dst-range 100.0.0.1-100.0.0.2 -m owner --gid-owner 404 -m tcp --dport 8080 -m addrtype --dst-type UNICAST -m comment --comment "808 - ipt_modules tests" -j REJECT --reject-with icmp-port-unreachable}) # rubocop:disable Layout/LineLength end + it 'recent set to set' do - expect(result.stdout).to match(%r{-A INPUT -d 30.0.0.0\/(8|255\.0\.0\.0) -m recent --set --name list1 (--mask 255.255.255.255 )?--rdest -m comment --comment "597 - recent set"}) + expect(result.stdout).to match(%r{-A INPUT -d 30.0.0.0/(8|255\.0\.0\.0) -m recent --set --name list1 (--mask 255.255.255.255 )?--rdest -m comment --comment "597 - recent set"}) end + it 'recent set to rcheck' do - expect(result.stdout).to match(%r{-A INPUT -d 30.0.0.0\/(8|255\.0\.0\.0) -m recent --rcheck --seconds 60 --hitcount 5 --rttl --name list1 (--mask 255.255.255.255 )?--rsource -m comment --comment "598 - recent rcheck"}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A INPUT -d 30.0.0.0/(8|255\.0\.0\.0) -m recent --rcheck --seconds 60 --hitcount 5 --rttl --name list1 (--mask 255.255.255.255 )?--rsource -m comment --comment "598 - recent rcheck"}) # rubocop:disable Layout/LineLength end + it 'recent set to update' do - expect(result.stdout).to match(%r{-A INPUT -d 30.0.0.0\/(8|255\.0\.0\.0) -m recent --update --name DEFAULT (--mask 255.255.255.255 )?--rsource -m comment --comment "599 - recent update"}) + expect(result.stdout).to match(%r{-A INPUT -d 30.0.0.0/(8|255\.0\.0\.0) -m recent --update --name DEFAULT (--mask 255.255.255.255 )?--rsource -m comment --comment "599 - recent update"}) end + it 'recent set to remove' do - expect(result.stdout).to match(%r{-A INPUT -d 30.0.0.0\/(8|255\.0\.0\.0) -m recent --remove --name DEFAULT (--mask 255.255.255.255 )?--rsource -m comment --comment "600 - recent remove"}) + expect(result.stdout).to match(%r{-A INPUT -d 30.0.0.0/(8|255\.0\.0\.0) -m recent --remove --name DEFAULT (--mask 255.255.255.255 )?--rsource -m comment --comment "600 - recent remove"}) end + it 'jump is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m comment --comment "567 - jump" -j TEST}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m comment --comment "567 - jump" -j TEST}) end + it 'notrack is set' do - notrack_rule = if os[:family] == 'redhat' && [5, 6].include?(os[:release].to_i) - '-A PREROUTING -p udp -m multiport --dports 53 -m comment --comment "004 do not track UDP connections to port 53" -j NOTRACK' - else - '-A PREROUTING -p udp -m multiport --dports 53 -m comment --comment "004 do not track UDP connections to port 53" -j CT --notrack' - end - expect(result.stdout).to match(%r{#{notrack_rule}}) + expect(result.stdout).to match(%r{-A PREROUTING -p (udp|17) -m udp --dport 53 -m comment --comment "004 do not track UDP connections to port 53" -j CT --notrack}) end end end diff --git a/spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb b/spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb index 3cf507c7f..1286f19bb 100644 --- a/spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb +++ b/spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb @@ -12,7 +12,7 @@ end end - describe 'standard attributes', unless: (os[:family] == 'redhat' && os[:release].start_with?('5', '6')) || (os[:family] == 'sles') do + describe 'standard attributes', unless: os[:family] == 'sles' do describe 'dst_range' do context 'when 2001::db8::1-2001:db8::ff' do pp = <<-PUPPETCODE @@ -20,20 +20,20 @@ class { '::firewall': } firewall { '602 - test': proto => tcp, dport => '602', - action => accept, - provider => 'ip6tables', + jump => accept, + protocol => 'ip6tables', dst_range => '2001::db8::1-2001:db8::ff', } PUPPETCODE it 'applies' do apply_manifest(pp, expect_failures: true) do |r| - expect(r.stderr).to match(%r{Invalid IP address "2001::db8::1" in range "2001::db8::1-2001:db8::ff"}) + expect(r.stderr).to match(%r{Invalid IP address `2001::db8::1` in range `2001::db8::1-2001:db8::ff`}) end end it 'does not contain the rule' do run_shell('ip6tables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m iprange --dst-range 2001::db8::1-2001:db8::ff -m multiport --ports 602 -m comment --comment "602 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m iprange --dst-range 2001::db8::1-2001:db8::ff -m multiport --ports 602 -m comment --comment "602 - test" -j ACCEPT}) end end end @@ -46,20 +46,20 @@ class { '::firewall': } class { '::firewall': } firewall { '603 - test': proto => tcp, - action => accept, + jump => accept, #{type} => 'BROKEN', - provider => 'ip6tables', + protocol => 'IPv6', } - PUPPETCODE + PUPPETCODE it 'fails' do apply_manifest(pp, expect_failures: true) do |r| - expect(r.stderr).to match(%r{Invalid value "BROKEN".}) + expect(r.stderr).to match(%r{Error: Parameter #{type} failed}) end end it 'does not contain the rule' do run_shell('ip6tables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype\s.*\sBROKEN -m comment --comment "603 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m addrtype\s.*\sBROKEN -m comment --comment "603 - test" -j ACCEPT}) end end end @@ -69,20 +69,20 @@ class { '::firewall': } class { '::firewall': } firewall { '619 - test': proto => tcp, - action => accept, + jump => accept, #{type} => ['LOCAL', 'LOCAL'], - provider => 'ip6tables', + protocol => 'IPv6', } - PUPPETCODE + PUPPETCODE it 'fails' do apply_manifest(pp, expect_failures: true) do |r| - expect(r.stderr).to match(%r{#{type} elements must be unique}) + expect(r.stderr).to match(%r{`#{type}` elements must be unique}) end end it 'does not contain the rule' do run_shell('ip6tables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL -m addrtype\s.*\sLOCAL -m comment --comment "619 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m addrtype\s.*\sLOCAL -m addrtype\s.*\sLOCAL -m comment --comment "619 - test" -j ACCEPT}) end end end @@ -92,11 +92,11 @@ class { '::firewall': } class { '::firewall': } firewall { '616 - test': proto => tcp, - action => accept, + jump => accept, #{type} => ['LOCAL', '! LOCAL'], - provider => 'ip6tables', + protocol => 'IPv6', } - PUPPETCODE + PUPPETCODE it 'fails' do apply_manifest(pp, expect_failures: true) do |r| expect(r.stderr).to match(%r{Multiple #{type} elements are available from iptables version}) @@ -105,7 +105,7 @@ class { '::firewall': } it 'does not contain the rule' do run_shell('ip6tables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "616 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "616 - test" -j ACCEPT}) end end end @@ -120,20 +120,20 @@ class { '::firewall': } ensure => present, proto => tcp, dport => '571', - action => accept, + jump => accept, hop_limit => 'invalid', - provider => 'ip6tables', + protocol => 'IPv6', } PUPPETCODE it 'applies' do apply_manifest(pp, expect_failures: true) do |r| - expect(r.stderr).to match(%r{Invalid value "invalid".}) + expect(r.stderr).to match(%r{Error: Parameter hop_limit failed}) end end it 'does not contain the rule' do run_shell('ip6tables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m multiport --dports 571 -m comment --comment "571 - test" -m hl --hl-eq invalid -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6)-m tcp --dport 571 -m comment --comment "571 - test" -m hl --hl-eq invalid -j ACCEPT}) end end end @@ -178,9 +178,9 @@ class { '::firewall': } ensure => present, chain => 'INPUT', proto => tcp, - action => drop, + jump => drop, ipset => ['blacklist src,dst', '! honeypot dst'], - provider => 'ip6tables', + protocol => 'IPv6', require => Exec['add honeypot'], } PUPPETCODE @@ -189,7 +189,7 @@ class { '::firewall': } it 'contains the rule' do run_shell('ip6tables-save') do |r| - expect(r.stdout).to match(%r{-A INPUT -p tcp -m set --match-set blacklist src,dst -m set ! --match-set honeypot dst -m comment --comment "612 - test" -j DROP}) + expect(r.stdout).to match(%r{-A INPUT -p (tcp|6) -m set --match-set blacklist src,dst -m set ! --match-set honeypot dst -m comment --comment "612 - test" -j DROP}) end end end @@ -201,20 +201,20 @@ class { '::firewall': } firewall { '601 - test': proto => tcp, dport => '601', - action => accept, - provider => 'ip6tables', + jump => accept, + protocol => 'ip6tables', src_range => '2001::db8::1-2001:db8::ff', } PUPPETCODE it 'applies' do apply_manifest(pp, expect_failures: true) do |r| - expect(r.stderr).to match(%r{Invalid IP address "2001::db8::1" in range "2001::db8::1-2001:db8::ff"}) + expect(r.stderr).to match(%r{Invalid IP address `2001::db8::1` in range `2001::db8::1-2001:db8::ff`}) end end it 'does not contain the rule' do run_shell('ip6tables-save') do |r| - expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m iprange --src-range 2001::db8::1-2001:db8::ff -m multiport --dports 601 -m comment --comment "601 - test" -j ACCEPT}) + expect(r.stdout).not_to match(%r{-A INPUT -p (tcp|6) -m iprange --src-range 2001::db8::1-2001:db8::ff-m tcp --dport 601 -m comment --comment "601 - test" -j ACCEPT}) end end end @@ -228,16 +228,16 @@ class { '::firewall': } firewall { '805 - time': proto => tcp, dport => '8080', - action => accept, + jump => accept, chain => 'OUTPUT', date_start => '2016-01-19T04:17:07', date_stop => '2038-01-19T04:17:07', time_start => '6:00', time_stop => '17:00:00', - month_days => '7', + month_days => 7, week_days => 'Tue', kernel_timezone => true, - provider => 'ip6tables', + protocol => 'ip6tables', } PUPPETCODE it 'applies' do @@ -247,7 +247,7 @@ class { '::firewall': } it 'contains the rule' do run_shell('ip6tables-save') do |r| expect(r.stdout).to match( - %r{-A OUTPUT -p tcp -m multiport --dports 8080 -m time --timestart 06:00:00 --timestop 17:00:00 --monthdays 7 --weekdays Tue --datestart 2016-01-19T04:17:07 --datestop 2038-01-19T04:17:07 --kerneltz -m comment --comment "805 - time" -j ACCEPT}, # rubocop:disable Layout/LineLength + %r{-A OUTPUT -p (tcp|6) -m tcp --dport 8080 -m time --timestart 06:00:00 --timestop 17:00:00 --monthdays 7 --weekdays Tue --datestart 2016-01-19T04:17:07 --datestop 2038-01-19T04:17:07 --kerneltz -m comment --comment "805 - time" -j ACCEPT}, # rubocop:disable Layout/LineLength ) end end @@ -256,113 +256,113 @@ class { '::firewall': } end end - describe 'unless redhat 5 happy path', unless: (os[:family] == 'redhat' && os[:release].start_with?('5')) do + describe 'happy path' do before(:all) do pp = <<-PUPPETCODE firewall { '701 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '701', - action => accept, + protocol => 'ip6tables', + chain => 'FORWARD', + proto => tcp, + dport => '701', + jump => accept, physdev_in => 'eth0', } firewall { '702 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '702', - action => accept, + protocol => 'ip6tables', + chain => 'FORWARD', + proto => tcp, + dport => '702', + jump => accept, physdev_out => 'eth1', } firewall { '703 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '703', - action => accept, - physdev_in => 'eth0', + protocol => 'IPv6', + chain => 'FORWARD', + proto => tcp, + dport => '703', + jump => accept, + physdev_in => 'eth0', physdev_out => 'eth1', } firewall { '704 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '704', - action => accept, + protocol => 'IPv6', + chain => 'FORWARD', + proto => tcp, + dport => '704', + jump => accept, physdev_is_bridged => true, } firewall { '705 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '705', - action => accept, + protocol => 'ip6tables', + chain => 'FORWARD', + proto => tcp, + dport => '705', + jump => accept, physdev_in => 'eth0', physdev_is_bridged => true, } firewall { '706 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '706', - action => accept, + protocol => 'ip6tables', + chain => 'FORWARD', + proto => tcp, + dport => '706', + jump => accept, physdev_out => 'eth1', physdev_is_bridged => true, } firewall { '707 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '707', - action => accept, - physdev_in => 'eth0', + protocol => 'ip6tables', + chain => 'FORWARD', + proto => tcp, + dport => '707', + jump => accept, + physdev_in => 'eth0', physdev_out => 'eth1', physdev_is_bridged => true, } firewall { '708 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '708', - action => accept, + protocol => 'ip6tables', + chain => 'FORWARD', + proto => tcp, + dport => '708', + jump => accept, physdev_is_in => true, } firewall { '709 - test': - provider => 'ip6tables', - chain => 'FORWARD', - proto => tcp, - dport => '709', - action => accept, + protocol => 'ip6tables', + chain => 'FORWARD', + proto => tcp, + dport => '709', + jump => accept, physdev_is_out => true, } firewall { '1002 - set_dscp': proto => 'tcp', jump => 'DSCP', set_dscp => '0x01', - dport => '997', + dport => '997', chain => 'OUTPUT', table => 'mangle', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '1003 EF - set_dscp_class': proto => 'tcp', jump => 'DSCP', - dport => '997', - set_dscp_class => 'EF', + dport => '997', + set_dscp_class => 'ef', chain => 'OUTPUT', table => 'mangle', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '502 - set_mss': proto => 'tcp', tcp_flags => 'SYN,RST SYN', jump => 'TCPMSS', - set_mss => '1360', + set_mss => 1360, mss => '1361:1541', chain => 'FORWARD', table => 'mangle', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '503 - clamp_mss_to_pmtu': proto => 'tcp', @@ -370,24 +370,24 @@ class { '::firewall': } tcp_flags => 'SYN,RST SYN', jump => 'TCPMSS', clamp_mss_to_pmtu => true, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '803 - hashlimit_upto test ip6': chain => 'INPUT', - provider => 'ip6tables', + protocol => 'ip6tables', hashlimit_name => 'upto-ip6', hashlimit_upto => '16/sec', - hashlimit_burst => '640', - hashlimit_htable_size => '1000000', - hashlimit_htable_max => '320000', - hashlimit_htable_expire => '36000000', - action => accept, + hashlimit_burst => 640, + hashlimit_htable_size => 1000000, + hashlimit_htable_max => 320000, + hashlimit_htable_expire => 36000000, + jump => accept, } firewall { '503 match_mark ip6tables - test': proto => 'all', match_mark => '0x1', - action => reject, - provider => 'ip6tables', + jump => reject, + protocol => 'ip6tables', } PUPPETCODE @@ -397,60 +397,76 @@ class { '::firewall': } let(:result) { run_shell('ip6tables-save') } it 'physdev_in is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-in eth0 -m multiport --dports 701 -m comment --comment "701 - test" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-in eth0 -m tcp --dport 701 -m comment --comment "701 - test" -j ACCEPT}) end + it 'physdev_out is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-out eth1 -m multiport --dports 702 -m comment --comment "702 - test" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-out eth1 -m tcp --dport 702 -m comment --comment "702 - test" -j ACCEPT}) end + it 'physdev_in and physdev_out is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-in eth0 --physdev-out eth1 -m multiport --dports 703 -m comment --comment "703 - test" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-in eth0 --physdev-out eth1 -m tcp --dport 703 -m comment --comment "703 - test" -j ACCEPT}) end + it 'physdev_is_bridged is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-is-bridged -m multiport --dports 704 -m comment --comment "704 - test" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-is-bridged -m tcp --dport 704 -m comment --comment "704 - test" -j ACCEPT}) end + it 'physdev_in and physdev_is_bridged is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-in eth0 --physdev-is-bridged -m multiport --dports 705 -m comment --comment "705 - test" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-in eth0 --physdev-is-bridged -m tcp --dport 705 -m comment --comment "705 - test" -j ACCEPT}) end + it 'physdev_out and physdev_is_bridged is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-out eth1 --physdev-is-bridged -m multiport --dports 706 -m comment --comment "706 - test" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-out eth1 --physdev-is-bridged -m tcp --dport 706 -m comment --comment "706 - test" -j ACCEPT}) end + it 'physdev_in and physdev_out and physdev_is_bridged is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-in eth0 --physdev-out eth1 --physdev-is-bridged -m multiport --dports 707 -m comment --comment "707 - test" -j ACCEPT}) + expect(result.stdout).to match( + %r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-in eth0 --physdev-out eth1 --physdev-is-bridged -m tcp --dport 707 -m comment --comment "707 - test" -j ACCEPT}, + ) end + it 'physdev_is_in is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-is-in -m multiport --dports 708 -m comment --comment "708 - test" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-is-in -m tcp --dport 708 -m comment --comment "708 - test" -j ACCEPT}) end + it 'physdev_is_out is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m physdev\s+--physdev-is-out -m multiport --dports 709 -m comment --comment "709 - test" -j ACCEPT}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m physdev\s+--physdev-is-out -m tcp --dport 709 -m comment --comment "709 - test" -j ACCEPT}) end + it 'set_dscp is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m multiport --dports 997 -m comment --comment "1002 - set_dscp" -j DSCP --set-dscp 0x01}) + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m tcp --dport 997 -m comment --comment "1002 - set_dscp" -j DSCP --set-dscp 0x01}) end + it 'set_dscp_class is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m multiport --dports 997 -m comment --comment "1003 EF - set_dscp_class" -j DSCP --set-dscp 0x2e}) + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m tcp --dport 997 -m comment --comment "1003 EF - set_dscp_class" -j DSCP --set-dscp 0x2e}) end + it 'set_mss and mss is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1541 -m comment --comment "502 - set_mss" -j TCPMSS --set-mss 1360}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m tcp --tcp-flags SYN,RST SYN -m tcpmss --mss 1361:1541 -m comment --comment "502 - set_mss" -j TCPMSS --set-mss 1360}) end + it 'clamp_mss_to_pmtu is set' do - expect(result.stdout).to match(%r{-A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -m comment --comment "503 - clamp_mss_to_pmtu" -j TCPMSS --clamp-mss-to-pmtu}) + expect(result.stdout).to match(%r{-A FORWARD -p (tcp|6) -m tcp --tcp-flags SYN,RST SYN -m comment --comment "503 - clamp_mss_to_pmtu" -j TCPMSS --clamp-mss-to-pmtu}) end + it 'hashlimit_name set to "upto-ip6"' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m hashlimit --hashlimit-upto 16\/sec --hashlimit-burst 640 --hashlimit-name upto-ip6 --hashlimit-htable-size 1000000 --hashlimit-htable-max 320000 --hashlimit-htable-expire 36000000 -m comment --comment "803 - hashlimit_upto test ip6" -j ACCEPT}) # rubocop:disable Layout/LineLength : Cannot reduce line to required length + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m hashlimit --hashlimit-upto 16/sec --hashlimit-burst 640 --hashlimit-name upto-ip6 --hashlimit-htable-size 1000000 --hashlimit-htable-max 320000 --hashlimit-htable-expire 36000000 -m comment --comment "803 - hashlimit_upto test ip6" -j ACCEPT}) # rubocop:disable Layout/LineLength : Cannot reduce line to required length end + it 'match_mark is set' do expect(result.stdout).to match(%r{-A INPUT -m mark --mark 0x1 -m comment --comment "503 match_mark ip6tables - test" -j REJECT --reject-with icmp6-port-unreachable}) end end - describe 'ishasmorefrags/islastfrag/isfirstfrag', unless: (os[:family] == 'redhat' && os[:release].start_with?('5', '6')) || (os[:family] == 'sles') do + describe 'ishasmorefrags/islastfrag/isfirstfrag', unless: os[:family] == 'sles' do shared_examples 'is idempotent' do |values, line_match| pp2 = <<-PUPPETCODE class { '::firewall': } firewall { '599 - test': ensure => present, proto => 'tcp', - provider => 'ip6tables', + protocol => 'IPv6', #{values} } PUPPETCODE @@ -468,7 +484,7 @@ class { '::firewall': } firewall { '599 - test': ensure => present, proto => 'tcp', - provider => 'ip6tables', + protocol => 'IPv6', #{values} } PUPPETCODE @@ -486,147 +502,179 @@ class { '::firewall': } before :all do ip6tables_flush_all_tables end - it_behaves_like 'is idempotent', '', %r{-A INPUT -p tcp -m comment --comment "599 - test"} + + it_behaves_like 'is idempotent', '', %r{-A INPUT -p (tcp|6) -m comment --comment "599 - test"} end + context 'when set to true' do before :all do ip6tables_flush_all_tables end + it_behaves_like 'is idempotent', 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', - %r{-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"} + %r{-A INPUT -p (tcp|6) -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"} end + context 'when set to false' do before :all do ip6tables_flush_all_tables end + it_behaves_like 'is idempotent', 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', %r{-A INPUT -p tcp -m comment --comment "599 - test"} end end + describe 'editing a rule' do context 'when unset or false' do before :each do ip6tables_flush_all_tables run_shell('ip6tables -A INPUT -p tcp -m comment --comment "599 - test"') end + context 'when current value is false' do - it_behaves_like "doesn't change", 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', %r{-A INPUT -p tcp -m comment --comment "599 - test"} + it_behaves_like "doesn't change", 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', %r{-A INPUT -p (tcp|6) -m comment --comment "599 - test"} end + context 'when current value is true' do it_behaves_like 'is idempotent', 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', - %r{-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"} + %r{-A INPUT -p (tcp|6) -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"} end end + context 'when set to true' do before :each do ip6tables_flush_all_tables run_shell('ip6tables -A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"') end + context 'when current value is false' do - it_behaves_like 'is idempotent', 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', %r{-A INPUT -p tcp -m comment --comment "599 - test"} + it_behaves_like 'is idempotent', 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', %r{-A INPUT -p (tcp|6) -m comment --comment "599 - test"} end + context 'when current value is true' do it_behaves_like "doesn't change", 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', - %r{-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"} + %r{-A INPUT -p (tcp|6) -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"} end end end end describe 'purge' do - context 'when ipv6 chain purge', unless: os[:family] == 'redhat' && os[:release].start_with?('5') do + context 'when ipv6 chain purge' do after(:all) do ip6tables_flush_all_tables end + before(:each) do ip6tables_flush_all_tables run_shell('ip6tables -A INPUT -p tcp -s 1::42') run_shell('ip6tables -A INPUT -p udp -s 1::42') + run_shell('ip6tables -A INPUT -s 1::49 -m comment --comment "009 output-1::49"') run_shell('ip6tables -A OUTPUT -s 1::50 -m comment --comment "010 output-1::50"') end let(:result) { run_shell('ip6tables-save') } pp1 = <<-PUPPETCODE - class { 'firewall': } - firewallchain { 'INPUT:filter:IPv6': - purge => true, - } - PUPPETCODE + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv6': + purge => true, + } + PUPPETCODE it 'purges only the specified chain' do apply_manifest(pp1, expect_changes: true) expect(result.stdout).to match(%r{010 output-1::50}) expect(result.stdout).not_to match(%r{1::42}) - expect(result.stderr).to eq('') end pp2 = <<-PUPPETCODE - class { 'firewall': } - firewallchain { 'OUTPUT:filter:IPv6': - purge => true, - } - firewall { '010 output-1::50': - chain => 'OUTPUT', - proto => 'all', - source => '1::50', - provider => 'ip6tables', - } - PUPPETCODE + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv6': + purge => true, + } + firewall { '009 input-1::49': + chain => 'INPUT', + proto => 'all', + source => '1::49', + protocol => 'IPv6', + } + PUPPETCODE it 'ignores managed rules' do - apply_manifest(pp2, catch_changes: true) + apply_manifest(pp2, expect_changes: true) + + expect(result.stdout).not_to match(%r{-s 1::42(/128)?}) + expect(result.stdout).to match(%r{"009 input-1::49"}) end pp3 = <<-PUPPETCODE - class { 'firewall': } - firewallchain { 'INPUT:filter:IPv6': - purge => true, - ignore => [ - '-s 1::42', - ], - } - PUPPETCODE + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv6': + purge => true, + ignore => [ + '-s 1::42', + ], + } + PUPPETCODE it 'ignores specified rules' do - apply_manifest(pp3, catch_changes: true) + apply_manifest(pp3, expect_changes: true) + + expect(result.stdout).to match(%r{-A INPUT -s 1::42(/128)? -p (tcp|6)\s?\n-A INPUT -s 1::42(/128)? -p (udp|17)}) + expect(result.stdout).not_to match(%r{009 output-1::49}) end pp4 = <<-PUPPETCODE - class { 'firewall': } - firewallchain { 'INPUT:filter:IPv6': - purge => true, - ignore => [ - '-s 1::42', - ], - } - firewall { '014 input-1::46': - chain => 'INPUT', - proto => 'all', - source => '1::46', - provider => 'ip6tables', - } - -> firewall { '013 input-1::45': - chain => 'INPUT', - proto => 'all', - source => '1::45', - provider => 'ip6tables', - } - -> firewall { '012 input-1::44': - chain => 'INPUT', - proto => 'all', - source => '1::44', - provider => 'ip6tables', - } - -> firewall { '011 input-1::43': - chain => 'INPUT', - proto => 'all', - source => '1::43', - provider => 'ip6tables', - } - PUPPETCODE + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv6': + purge => true, + ignore_foreign => true, + } + PUPPETCODE + it 'ignores foreign rules' do + apply_manifest(pp4, expect_changes: true) + + expect(result.stdout).to match(%r{-A INPUT -s 1::42(/128)? -p (tcp|6)\s?\n-A INPUT -s 1::42(/128)? -p (udp|17)}) + expect(result.stdout).not_to match(%r{009 output-1::49}) + end + + pp5 = <<-PUPPETCODE + class { 'firewall': } + firewallchain { 'INPUT:filter:IPv6': + purge => true, + ignore => [ + '-s 1::42', + ], + } + firewall { '014 input-1::46': + chain => 'INPUT', + proto => 'all', + source => '1::46', + protocol => 'IPv6', + } + -> firewall { '013 input-1::45': + chain => 'INPUT', + proto => 'all', + source => '1::45', + protocol => 'IPv6', + } + -> firewall { '012 input-1::44': + chain => 'INPUT', + proto => 'all', + source => '1::44', + protocol => 'IPv6', + } + -> firewall { '011 input-1::43': + chain => 'INPUT', + proto => 'all', + source => '1::43', + protocol => 'IPv6', + } + PUPPETCODE it 'adds managed rules with ignored rules' do - apply_manifest(pp4, catch_failures: true) + apply_manifest(pp5, catch_failures: true) - expect(result.stdout).to match(%r{-A INPUT -s 1::42(\/128)? -p tcp\s?\n-A INPUT -s 1::42(\/128)? -p udp}) + expect(result.stdout).to match(%r{-A INPUT -s 1::42(/128)? -p (tcp|6)\s?\n-A INPUT -s 1::42(/128)? -p (udp|17)}) end end end diff --git a/spec/acceptance/firewall_attributes_ipv6_happy_path_spec.rb b/spec/acceptance/firewall_attributes_ipv6_happy_path_spec.rb index 0515c5cf7..830bb1fd3 100644 --- a/spec/acceptance/firewall_attributes_ipv6_happy_path_spec.rb +++ b/spec/acceptance/firewall_attributes_ipv6_happy_path_spec.rb @@ -10,129 +10,122 @@ describe 'attributes test' do before(:all) do - # On RHEL 9 this must be lower case, on all others it must be upper case - mac_source = if os[:family] == 'redhat' && os[:release].start_with?('9') - '0a:1b:3c:4d:5e:6f' - else - '0A:1B:3C:4D:5E:6F' - end - pp = <<-PUPPETCODE class { '::firewall': } firewall { '571 - hop_limit': - ensure => present, - proto => tcp, - dport => '571', - action => accept, + ensure => present, + proto => tcp, + dport => '571', + jump => accept, hop_limit => '5', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '576 - checksum_fill': - proto => udp, - table => 'mangle', - outiface => 'virbr0', - chain => 'POSTROUTING', - dport => '68', - jump => 'CHECKSUM', + proto => udp, + table => 'mangle', + outiface => 'virbr0', + chain => 'POSTROUTING', + dport => '68', + jump => 'CHECKSUM', checksum_fill => true, - provider => ip6tables, + protocol => ip6tables, } firewall { '587 - ishasmorefrags true': - ensure => present, - proto => tcp, - dport => '587', - action => accept, + ensure => present, + proto => tcp, + dport => '587', + jump => accept, ishasmorefrags => true, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '588 - ishasmorefrags false': - ensure => present, - proto => tcp, - dport => '588', - action => accept, + ensure => present, + proto => tcp, + dport => '588', + jump => accept, ishasmorefrags => false, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '589 - islastfrag true': - ensure => present, - proto => tcp, - dport => '589', - action => accept, + ensure => present, + proto => tcp, + dport => '589', + jump => accept, islastfrag => true, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '590 - islastfrag false': - ensure => present, - proto => tcp, - dport => '590', - action => accept, + ensure => present, + proto => tcp, + dport => '590', + jump => accept, islastfrag => false, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '591 - isfirstfrag true': - ensure => present, - proto => tcp, - dport => '591', - action => accept, + ensure => present, + proto => tcp, + dport => '591', + jump => accept, isfirstfrag => true, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '592 - isfirstfrag false': - ensure => present, - proto => tcp, - dport => '592', - action => accept, + ensure => present, + proto => tcp, + dport => '592', + jump => accept, isfirstfrag => false, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '593 - tcpfrags': - proto => tcp, - action => accept, + proto => tcp, + jump => accept, tcp_flags => 'FIN,SYN ACK', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '601 - src_range': proto => tcp, - dport => '601', - action => accept, + dport => '601', + jump => accept, src_range => '2001:db8::1-2001:db8::ff', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '602 - dst_range': proto => tcp, - dport => '602', - action => accept, + dport => '602', + jump => accept, dst_range => '2001:db8::1-2001:db8::ff', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '604 - mac_source': ensure => present, source => '2001:db8::1/128', - mac_source => '#{mac_source}', + mac_source => '0A:1B:3C:4D:5E:6F', chain => 'INPUT', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '605 - socket true': ensure => present, proto => tcp, - dport => '605', - action => accept, + dport => '605', + jump => accept, chain => 'INPUT', socket => true, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '606 - socket false': ensure => present, proto => tcp, - dport => '606', - action => accept, + dport => '606', + jump => accept, chain => 'INPUT', socket => false, - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '607 - ipsec_policy ipsec': ensure => 'present', - action => 'reject', + jump => 'reject', chain => 'OUTPUT', destination => '2001:db8::1/128', ipsec_dir => 'out', @@ -140,11 +133,11 @@ class { '::firewall': } proto => 'all', reject => 'icmp6-adm-prohibited', table => 'filter', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '608 - ipsec_policy none': ensure => 'present', - action => 'reject', + jump => 'reject', chain => 'OUTPUT', destination => '2001:db8::1/128', ipsec_dir => 'out', @@ -152,11 +145,11 @@ class { '::firewall': } proto => 'all', reject => 'icmp6-adm-prohibited', table => 'filter', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '609 - ipsec_dir out': ensure => 'present', - action => 'reject', + jump => 'reject', chain => 'OUTPUT', destination => '2001:db8::1/128', ipsec_dir => 'out', @@ -164,11 +157,11 @@ class { '::firewall': } proto => 'all', reject => 'icmp6-adm-prohibited', table => 'filter', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '610 - ipsec_dir in': ensure => 'present', - action => 'reject', + jump => 'reject', chain => 'INPUT', destination => '2001:db8::1/128', ipsec_dir => 'in', @@ -176,60 +169,60 @@ class { '::firewall': } proto => 'all', reject => 'icmp6-adm-prohibited', table => 'filter', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '611 - set_mark': - ensure => present, - chain => 'OUTPUT', - proto => tcp, - dport => '611', - jump => 'MARK', - table => 'mangle', + ensure => present, + chain => 'OUTPUT', + proto => tcp, + dport => '611', + jump => 'MARK', + table => 'mangle', set_mark => '0x3e8/0xffffffff', - provider => 'ip6tables', + protocol => 'ip6tables', } firewall { '613 - dst_type MULTICAST': proto => tcp, - action => accept, - dst_type => 'MULTICAST', - provider => 'ip6tables', + jump => accept, + dst_type => 'MULTICAST', + protocol => 'ip6tables', } firewall { '614 - src_type MULTICAST': proto => tcp, - action => accept, - src_type => 'MULTICAST', - provider => 'ip6tables', + jump => accept, + src_type => 'MULTICAST', + protocol => 'ip6tables', } firewall { '615 - dst_type ! MULTICAST': proto => tcp, - action => accept, - dst_type => '! MULTICAST', - provider => 'ip6tables', + jump => accept, + dst_type => '! MULTICAST', + protocol => 'ip6tables', } firewall { '616 - src_type ! MULTICAST': proto => tcp, - action => accept, - src_type => '! MULTICAST', - provider => 'ip6tables', + jump => accept, + src_type => '! MULTICAST', + protocol => 'ip6tables', } firewall { '619 - dst_type multiple values': proto => tcp, - action => accept, - dst_type => ['LOCAL', '! LOCAL'], - provider => 'ip6tables', + jump => accept, + dst_type => ['LOCAL', '! LOCAL'], + protocol => 'ip6tables', } firewall { '620 - src_type multiple values': proto => tcp, - action => accept, - src_type => ['LOCAL', '! LOCAL'], - provider => 'ip6tables', + jump => accept, + src_type => ['LOCAL', '! LOCAL'], + protocol => 'ip6tables', } firewall { '801 - ipt_modules tests': proto => tcp, dport => '8080', - action => reject, + jump => reject, chain => 'OUTPUT', - provider => 'ip6tables', + protocol => 'ip6tables', uid => 0, gid => 404, src_range => "2001::-2002::", @@ -243,9 +236,9 @@ class { '::firewall': } firewall { '802 - ipt_modules tests': proto => tcp, dport => '8080', - action => reject, + jump => reject, chain => 'OUTPUT', - provider => 'ip6tables', + protocol => 'ip6tables', gid => 404, dst_range => "2003::-2004::", dst_type => 'UNICAST', @@ -254,30 +247,30 @@ class { '::firewall': } } firewall { '806 - hashlimit_above test ipv6': chain => 'INPUT', - provider => 'ip6tables', + protocol => 'ip6tables', proto => 'tcp', hashlimit_name => 'above-ip6', hashlimit_above => '526/sec', - hashlimit_htable_gcinterval => '10', + hashlimit_htable_gcinterval => 10, hashlimit_mode => 'srcip,dstip', - action => accept, + jump => accept, } firewall { '811 - tee_gateway6': chain => 'PREROUTING', table => 'mangle', jump => 'TEE', gateway => '2001:db8::1', - proto => all, - provider => 'ip6tables', + proto => all, + protocol => 'ip6tables', } firewall { '812 - hex_string': chain => 'INPUT', proto => 'tcp', string_hex => '|f4 6d 04 25 b2 02 00 0a|', string_algo => 'kmp', - string_to => '65535', - action => accept, - provider => 'ip6tables', + string_to => 65534, + jump => accept, + protocol => 'ip6tables', } firewall { '500 allow v6 non-any queries': chain => 'OUTPUT', @@ -285,119 +278,150 @@ class { '::firewall': } dport => '53', string_hex => '! |0000ff0001|', string_algo => 'bm', - to => '65535', - action => 'accept', - provider => 'ip6tables', + string_to => 65534, + jump => 'accept', + protocol => 'ip6tables', } PUPPETCODE idempotent_apply(pp) end + let(:result) { run_shell('ip6tables-save') } it 'hop_limit is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 571 -m hl --hl-eq 5 -m comment --comment "571 - hop_limit" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 571 -m hl --hl-eq 5 -m comment --comment "571 - hop_limit" -j ACCEPT}) end + it 'checksum_fill is set' do - expect(result.stdout).to match(%r{-A POSTROUTING -o virbr0 -p udp -m multiport --dports 68 -m comment --comment "576 - checksum_fill" -j CHECKSUM --checksum-fill}) + expect(result.stdout).to match(%r{-A POSTROUTING -o virbr0 -p (udp|17) -m udp --dport 68 -m comment --comment "576 - checksum_fill" -j CHECKSUM --checksum-fill}) end + it 'ishasmorefrags when true' do - expect(result.stdout).to match(%r{A INPUT -p tcp -m frag --fragid 0 --fragmore -m multiport --dports 587 -m comment --comment "587 - ishasmorefrags true" -j ACCEPT}) + expect(result.stdout).to match(%r{A INPUT -p (tcp|6) -m frag --fragid 0 --fragmore -m tcp --dport 587 -m comment --comment "587 - ishasmorefrags true" -j ACCEPT}) end + it 'ishasmorefrags when false' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 588 -m comment --comment "588 - ishasmorefrags false" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 588 -m comment --comment "588 - ishasmorefrags false" -j ACCEPT}) end + it 'islastfrag when true' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m frag --fragid 0 --fraglast -m multiport --dports 589 -m comment --comment "589 - islastfrag true" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m frag --fragid 0 --fraglast -m tcp --dport 589 -m comment --comment "589 - islastfrag true" -j ACCEPT}) end + it 'islastfrag when false' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 590 -m comment --comment "590 - islastfrag false" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 590 -m comment --comment "590 - islastfrag false" -j ACCEPT}) end + it 'isfirstfrag when true' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m frag --fragid 0 --fragfirst -m multiport --dports 591 -m comment --comment "591 - isfirstfrag true" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m frag --fragid 0 --fragfirst -m tcp --dport 591 -m comment --comment "591 - isfirstfrag true" -j ACCEPT}) end + it 'isfirstfrag when false' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 592 -m comment --comment "592 - isfirstfrag false" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 592 -m comment --comment "592 - isfirstfrag false" -j ACCEPT}) end + it 'tcp_flags is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN ACK -m comment --comment "593 - tcpfrags" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --tcp-flags FIN,SYN ACK -m comment --comment "593 - tcpfrags" -j ACCEPT}) end + it 'src_range is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m iprange --src-range 2001:db8::1-2001:db8::ff -m multiport --dports 601 -m comment --comment "601 - src_range" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m iprange --src-range 2001:db8::1-2001:db8::ff -m tcp --dport 601 -m comment --comment "601 - src_range" -j ACCEPT}) end + it 'dst_range is set' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m iprange --dst-range 2001:db8::1-2001:db8::ff -m multiport --dports 602 -m comment --comment "602 - dst_range" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m iprange --dst-range 2001:db8::1-2001:db8::ff -m tcp --dport 602 -m comment --comment "602 - dst_range" -j ACCEPT}) end + it 'mac_source is set' do - expect(result.stdout).to match(%r{-A INPUT -s 2001:db8::1\/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -p tcp -m mac --mac-source 0(a|A):1(b|B):3(c|C):4(d|D):5(e|E):6(f|F) -m comment --comment "604 - mac_source"}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A INPUT -s 2001:db8::1/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -p (tcp|6) -m mac --mac-source 0(a|A):1(b|B):3(c|C):4(d|D):5(e|E):6(f|F) -m comment --comment "604 - mac_source"}) # rubocop:disable Layout/LineLength end + it 'socket when true' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 605 -m socket -m comment --comment "605 - socket true" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 605 -m socket -m comment --comment "605 - socket true" -j ACCEPT}) end + it 'socket when false' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m multiport --dports 606 -m comment --comment "606 - socket false" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m tcp --dport 606 -m comment --comment "606 - socket false" -j ACCEPT}) end + it 'ipsec_policy when ipsec' do expect(result.stdout).to match( - %r{-A OUTPUT -d 2001:db8::1\/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -m policy --dir out --pol ipsec -m comment --comment "607 - ipsec_policy ipsec" -j REJECT --reject-with icmp6-adm-prohibited}, # rubocop:disable Layout/LineLength + %r{-A OUTPUT -d 2001:db8::1/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -m policy --dir out --pol ipsec -m comment --comment "607 - ipsec_policy ipsec" -j REJECT --reject-with icmp6-adm-prohibited}, # rubocop:disable Layout/LineLength ) end + it 'ipsec_policy when none' do expect(result.stdout).to match( - %r{-A OUTPUT -d 2001:db8::1\/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -m policy --dir out --pol none -m comment --comment "608 - ipsec_policy none" -j REJECT --reject-with icmp6-adm-prohibited}, # rubocop:disable Layout/LineLength + %r{-A OUTPUT -d 2001:db8::1/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -m policy --dir out --pol none -m comment --comment "608 - ipsec_policy none" -j REJECT --reject-with icmp6-adm-prohibited}, # rubocop:disable Layout/LineLength ) end + it 'ipsec_dir when out' do expect(result.stdout).to match( - %r{-A OUTPUT -d 2001:db8::1\/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -m policy --dir out --pol ipsec -m comment --comment "609 - ipsec_dir out" -j REJECT --reject-with icmp6-adm-prohibited}, # rubocop:disable Layout/LineLength + %r{-A OUTPUT -d 2001:db8::1/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -m policy --dir out --pol ipsec -m comment --comment "609 - ipsec_dir out" -j REJECT --reject-with icmp6-adm-prohibited}, # rubocop:disable Layout/LineLength ) end + it 'ipsec_dir when in' do expect(result.stdout).to match( - %r{-A INPUT -d 2001:db8::1\/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -m policy --dir in --pol none -m comment --comment "610 - ipsec_dir in" -j REJECT --reject-with icmp6-adm-prohibited}, + %r{-A INPUT -d 2001:db8::1/(128|ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff) -m policy --dir in --pol none -m comment --comment "610 - ipsec_dir in" -j REJECT --reject-with icmp6-adm-prohibited}, ) end + it 'set_mark is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m multiport --dports 611 -m comment --comment "611 - set_mark" -j MARK --set-xmark 0x3e8\/0xffffffff}) + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m tcp --dport 611 -m comment --comment "611 - set_mark" -j MARK --set-xmark 0x3e8/0xffffffff}) end + it 'dst_type when MULTICAST' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m addrtype\s--dst-type\sMULTICAST -m comment --comment "613 - dst_type MULTICAST" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype\s--dst-type\sMULTICAST -m comment --comment "613 - dst_type MULTICAST" -j ACCEPT}) end + it 'src_type when MULTICAST' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m addrtype\s--src-type\sMULTICAST -m comment --comment "614 - src_type MULTICAST" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype\s--src-type\sMULTICAST -m comment --comment "614 - src_type MULTICAST" -j ACCEPT}) end + it 'dst_type when ! MULTICAST' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m addrtype( !\s--dst-type\sMULTICAST|\s--dst-type\s! MULTICAST) -m comment --comment "615 - dst_type ! MULTICAST" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype( !\s--dst-type\sMULTICAST|\s--dst-type\s! MULTICAST) -m comment --comment "615 - dst_type ! MULTICAST" -j ACCEPT}) end + it 'src_type when ! MULTICAST' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m addrtype( !\s--src-type\sMULTICAST|\s--src-type\s! MULTICAST) -m comment --comment "616 - src_type ! MULTICAST" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype( !\s--src-type\sMULTICAST|\s--src-type\s! MULTICAST) -m comment --comment "616 - src_type ! MULTICAST" -j ACCEPT}) end + it 'dst_type when multiple values' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m addrtype --dst-type LOCAL -m addrtype ! --dst-type LOCAL -m comment --comment "619 - dst_type multiple values" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype --dst-type LOCAL -m addrtype ! --dst-type LOCAL -m comment --comment "619 - dst_type multiple values" -j ACCEPT}) end + it 'src_type when multiple values' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m addrtype --src-type LOCAL -m addrtype ! --src-type LOCAL -m comment --comment "620 - src_type multiple values" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m addrtype --src-type LOCAL -m addrtype ! --src-type LOCAL -m comment --comment "620 - src_type multiple values" -j ACCEPT}) end + it 'all the modules with multiple args is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m physdev\s+--physdev-in eth0 --physdev-out eth1 --physdev-is-bridged -m iprange --src-range 2001::-2002::\s+--dst-range 2003::-2004:: -m owner --uid-owner (0|root) --gid-owner 404 -m multiport --dports 8080 -m addrtype --src-type LOCAL --dst-type UNICAST -m comment --comment "801 - ipt_modules tests" -j REJECT --reject-with icmp6-port-unreachable}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m physdev\s+--physdev-in eth0 --physdev-out eth1 --physdev-is-bridged -m iprange --src-range 2001::-2002::\s+--dst-range 2003::-2004:: -m owner --uid-owner (0|root) --gid-owner 404 -m tcp --dport 8080 -m addrtype --src-type LOCAL -m addrtype --dst-type UNICAST -m comment --comment "801 - ipt_modules tests" -j REJECT --reject-with icmp6-port-unreachable}) # rubocop:disable Layout/LineLength end + it 'all the modules with single args is set' do - expect(result.stdout).to match(%r{-A OUTPUT -p tcp -m physdev\s+--physdev-out eth1 --physdev-is-bridged -m iprange --dst-range 2003::-2004:: -m owner --gid-owner 404 -m multiport --dports 8080 -m addrtype --dst-type UNICAST -m comment --comment "802 - ipt_modules tests" -j REJECT --reject-with icmp6-port-unreachable}) # rubocop:disable Layout/LineLength + expect(result.stdout).to match(%r{-A OUTPUT -p (tcp|6) -m physdev\s+--physdev-out eth1 --physdev-is-bridged -m iprange --dst-range 2003::-2004:: -m owner --gid-owner 404 -m tcp --dport 8080 -m addrtype --dst-type UNICAST -m comment --comment "802 - ipt_modules tests" -j REJECT --reject-with icmp6-port-unreachable}) # rubocop:disable Layout/LineLength end + it 'tee_gateway is set' do expect(result.stdout).to match(%r{-A PREROUTING -m comment --comment "811 - tee_gateway6" -j TEE --gateway 2001:db8::1}) end + it 'hashlimit_above is set' do - regex_array = [%r{-A INPUT}, %r{-p tcp}, %r{--hashlimit-above 526\/sec}, %r{--hashlimit-mode srcip,dstip}, + regex_array = [%r{-A INPUT}, %r{-p (tcp|6)}, %r{--hashlimit-above 526/sec}, %r{--hashlimit-mode srcip,dstip}, %r{--hashlimit-name above-ip6}, %r{--hashlimit-htable-gcinterval 10}, %r{-j ACCEPT}] regex_array.each do |regex| expect(result.stdout).to match(regex) end end + it 'checks hex_string value' do - expect(result.stdout).to match(%r{-A INPUT -p tcp -m string --hex-string "|f46d0425b202000a|" --algo kmp --to 65535 -m comment --comment "812 - hex_string" -j ACCEPT}) + expect(result.stdout).to match(%r{-A INPUT -p (tcp|6) -m string --hex-string "|f46d0425b202000a|" --algo kmp --to 65535 -m comment --comment "812 - hex_string" -j ACCEPT}) end + it 'checks hex_string value which include negation operator' do - regex_string = %r{-A OUTPUT -p udp -m multiport --dports 53 -m string ! --hex-string "|0000ff0001|" --algo bm --to 65535 -m comment --comment "500 allow v6 non-any queries" -j ACCEPT} + regex_string = %r{-A OUTPUT -p (udp|17) -m udp --dport 53 -m string ! --hex-string "|0000ff0001|" --algo bm --to 65535 -m comment --comment "500 allow v6 non-any queries" -j ACCEPT} expect(result.stdout).to match(regex_string) end end diff --git a/spec/acceptance/firewall_duplicate_comment_spec.rb b/spec/acceptance/firewall_duplicate_comment_spec.rb index 93251d07b..b27396bd9 100644 --- a/spec/acceptance/firewall_duplicate_comment_spec.rb +++ b/spec/acceptance/firewall_duplicate_comment_spec.rb @@ -4,9 +4,7 @@ describe 'firewall - duplicate comments' do before(:all) do - if os[:family] == 'ubuntu' || os[:family] == 'debian' - update_profile_file - end + update_profile_file if os[:family] == 'ubuntu' || os[:family] == 'debian' end after(:each) do @@ -21,9 +19,9 @@ class { 'firewall': } } firewall { '550 destination': - proto => tcp, - dport => '550', - action => accept, + proto => tcp, + dport => '550', + jump => accept, destination => '192.168.2.0/24', } PUPPETCODE @@ -33,7 +31,7 @@ class { 'firewall': } run_shell('iptables -I INPUT -m state --state NEW -m tcp -p tcp --dport 552 -j ACCEPT -m comment --comment "550 destination"') apply_manifest(pp) do |r| - expect(r.stderr).to include('Duplicate rule found for 550 destination. Skipping update.') + expect(r.stderr).to include('Duplicate names have been found within your Firewalls. This prevents the module from working correctly and must be manually resolved.') end end end diff --git a/spec/acceptance/firewallchain_spec.rb b/spec/acceptance/firewallchain_spec.rb index facf204c7..3e15bc3e9 100644 --- a/spec/acceptance/firewallchain_spec.rb +++ b/spec/acceptance/firewallchain_spec.rb @@ -8,7 +8,7 @@ ip6tables_flush_all_tables end - describe 'ensure' do + describe 'IPv4' do context 'when present' do pp1 = <<-PUPPETCODE firewallchain { 'MY_CHAIN:filter:IPv4': @@ -46,9 +46,47 @@ end end + describe 'IPv6' do + context 'when present' do + pp3 = <<-PUPPETCODE + firewallchain { 'MY_CHAIN:filter:IPv6': + ensure => present, + } + PUPPETCODE + it 'applies cleanly' do + # Run it twice and test for idempotency + idempotent_apply(pp3) + end + + it 'finds the chain' do + run_shell('ip6tables-save') do |r| + expect(r.stdout).to match(%r{MY_CHAIN}) + end + end + end + + context 'when absent' do + pp4 = <<-PUPPETCODE + firewallchain { 'MY_CHAIN:filter:IPv6': + ensure => absent, + } + PUPPETCODE + it 'applies cleanly' do + # Run it twice and test for idempotency + idempotent_apply(pp4) + end + + it 'fails to find the chain' do + run_shell('ip6tables-save') do |r| + expect(r.stdout).not_to match(%r{MY_CHAIN}) + end + end + end + end + # XXX purge => false is not yet implemented # context 'when adding a firewall rule to a chain:' do - # pp3 = <<-PUPPETCODE + # pp5 = <<-PUPPETCODE # firewallchain { 'MY_CHAIN:filter:IPv4': # ensure => present, # } @@ -61,13 +99,13 @@ # PUPPETCODE # it 'applies cleanly' do # # Run it twice and test for idempotency - # apply_manifest(pp3, :catch_failures => true) - # apply_manifest(pp3, :catch_changes => do_catch_changes) + # apply_manifest(pp5, :catch_failures => true) + # apply_manifest(pp5, :catch_changes => do_catch_changes) # end # end # context 'when not purge firewallchain chains:' do - # pp4 = <<-PUPPETCODE + # pp6 = <<-PUPPETCODE # firewallchain { 'MY_CHAIN:filter:IPv4': # ensure => present, # purge => false, @@ -79,14 +117,14 @@ # PUPPETCODE # it 'does not purge the rule' do # # Run it twice and test for idempotency - # apply_manifest(pp4, :catch_failures => true) do |r| + # apply_manifest(pp6, :catch_failures => true) do |r| # expect(r.stdout).to_not match(/removed/) # expect(r.stderr).to eq('') # end - # apply_manifest(pp4, :catch_changes => do_catch_changes) + # apply_manifest(pp6, :catch_changes => do_catch_changes) # end - # pp5 = <<-PUPPETCODE + # pp7 = <<-PUPPETCODE # firewall { '100 my rule': # chain => 'MY_CHAIN', # action => 'accept', @@ -96,7 +134,7 @@ # PUPPETCODE # it 'still has the rule' do # # Run it twice and test for idempotency - # apply_manifest(pp5, :catch_changes => do_catch_changes) + # apply_manifest(pp7, :catch_changes => do_catch_changes) # end # end @@ -106,14 +144,14 @@ end context 'when DROP' do - pp6 = <<-PUPPETCODE + pp8 = <<-PUPPETCODE firewallchain { 'FORWARD:filter:IPv4': policy => 'drop', } PUPPETCODE it 'applies cleanly' do # Run it twice and test for idempotency - idempotent_apply(pp6) + idempotent_apply(pp8) end it 'finds the chain' do diff --git a/spec/acceptance/resource_cmd_spec.rb b/spec/acceptance/resource_cmd_spec.rb index a8affbf0b..182f70977 100644 --- a/spec/acceptance/resource_cmd_spec.rb +++ b/spec/acceptance/resource_cmd_spec.rb @@ -9,7 +9,7 @@ before(:all) do # In order to properly check stderr for anomalies we need to fix the deprecation warnings from puppet.conf. config = run_shell('puppet config print config').stdout - run_shell("sed -i -e \'s/^templatedir.*$//\' #{config}") + run_shell("sed -i -e 's/^templatedir.*$//' #{config}") if fetch_os_name == 'redhat' && [6, 7].include?(os[:release].to_i) run_shell('echo export LC_ALL="C" > /etc/profile.d/my-custom.lang.sh') run_shell('echo "## US English ##" >> /etc/profile.d/my-custom.lang.sh') @@ -24,6 +24,11 @@ end context 'when make sure it returns no errors when executed on a clean machine' do + before(:all) do + iptables_flush_all_tables + ip6tables_flush_all_tables + end + run_shell('locale') let(:result) { run_shell('puppet resource firewall') } @@ -145,7 +150,7 @@ end end - context 'when accepts rules with negation' do + context 'when accepts rules with --dir' do before :all do iptables_flush_all_tables run_shell('iptables -t nat -A POSTROUTING -s 192.168.122.0/24 -m policy --dir out --pol ipsec -j ACCEPT') @@ -197,10 +202,9 @@ end end - # version of iptables that ships with el5 doesn't work with the # ip6tables provider # TODO: Test below fails if this file is run seperately. i.e. bundle exec rspec spec/acceptance/resource_cmd_spec.rb - context 'when dport/sport with ip6tables', unless: os[:family] == 'redhat' && os[:release].start_with?('5') do + context 'when dport/sport with ip6tables' do before :all do if os['family'] == 'debian' run_shell('echo "iptables-persistent iptables-persistent/autosave_v4 boolean false" | debconf-set-selections') diff --git a/spec/acceptance/rules_spec.rb b/spec/acceptance/rules_spec.rb index 5b5dfeb07..1925fa18a 100644 --- a/spec/acceptance/rules_spec.rb +++ b/spec/acceptance/rules_spec.rb @@ -6,9 +6,7 @@ describe 'rules spec' do describe 'complex ruleset 1' do before :all do - if os[:family] == 'redhat' - pre_setup - end + pre_setup if os[:family] == 'redhat' iptables_flush_all_tables ip6tables_flush_all_tables end @@ -26,31 +24,31 @@ proto => 'all', source => '10.0.0.0/8', destination => '10.0.0.0/8', - action => 'accept', + jump => 'ACCEPT', } firewall { '100 forward standard allow tcp': chain => 'FORWARD', source => '10.0.0.0/8', - destination => '!10.0.0.0/8', + destination => '! 10.0.0.0/8', proto => 'tcp', - ctstate => 'NEW', - sport => [80,443,21,20,22,53,123,43,873,25,465], - action => 'accept', + ctstate => 'NEW', + sport => ['80','443','21','20','22','53','123','43','873','25','465'], + jump => 'ACCEPT', } firewall { '100 forward standard allow udp': chain => 'FORWARD', source => '10.0.0.0/8', - destination => '!10.0.0.0/8', + destination => '! 10.0.0.0/8', proto => 'udp', - sport => [53,123], - action => 'accept', + sport => ['53','123'], + jump => 'ACCEPT', } firewall { '100 forward standard allow icmp': chain => 'FORWARD', source => '10.0.0.0/8', - destination => '!10.0.0.0/8', + destination => '! 10.0.0.0/8', proto => 'icmp', - action => 'accept', + jump => 'ACCEPT', } firewall { '090 ignore ipsec': @@ -59,28 +57,28 @@ outiface => 'eth0', ipsec_policy => 'ipsec', ipsec_dir => 'out', - action => 'accept', + jump => 'ACCEPT', } firewall { '093 ignore 10.0.0.0/8': table => 'nat', chain => 'POSTROUTING', outiface => 'eth0', destination => '10.0.0.0/8', - action => 'accept', + jump => 'ACCEPT', } firewall { '093 ignore 172.16.0.0/12': table => 'nat', chain => 'POSTROUTING', outiface => 'eth0', destination => '172.16.0.0/12', - action => 'accept', + jump => 'ACCEPT', } firewall { '093 ignore 192.168.0.0/16': table => 'nat', chain => 'POSTROUTING', outiface => 'eth0', destination => '192.168.0.0/16', - action => 'accept', + jump => 'ACCEPT', } firewall { '100 masq outbound': table => 'nat', @@ -101,12 +99,13 @@ it 'applies cleanly' do idempotent_apply(pp1) end + regex_values = [ %r{INPUT ACCEPT}, %r{FORWARD ACCEPT}, %r{OUTPUT ACCEPT}, - %r{-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) -d 10.0.0.0\/(8|255\.0\.0\.0) -m comment --comment \"090 forward allow local\" -j ACCEPT}, - %r{-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p icmp -m comment --comment \"100 forward standard allow icmp\" -j ACCEPT}, - %r{-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p tcp -m multiport --sports 80,443,21,20,22,53,123,43,873,25,465 -m conntrack --ctstate NEW -m comment --comment \"100 forward standard allow tcp\" -j ACCEPT}, # rubocop:disable Layout/LineLength - %r{-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p udp -m multiport --sports 53,123 -m comment --comment \"100 forward standard allow udp\" -j ACCEPT} + %r{-A FORWARD -s 10.0.0.0/(8|255\.0\.0\.0) -d 10.0.0.0/(8|255\.0\.0\.0) -m comment --comment "090 forward allow local" -j ACCEPT}, + %r{-A FORWARD -s 10.0.0.0/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0/(8|255\.0\.0\.0) -p (icmp|1) -m comment --comment "100 forward standard allow icmp" -j ACCEPT}, + %r{-A FORWARD -s 10.0.0.0/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0/(8|255\.0\.0\.0) -p (tcp|6) -m multiport --sports 80,443,21,20,22,53,123,43,873,25,465 -m conntrack --ctstate NEW -m comment --comment "100 forward standard allow tcp" -j ACCEPT}, # rubocop:disable Layout/LineLength + %r{-A FORWARD -s 10.0.0.0/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0/(8|255\.0\.0\.0) -p (udp|17) -m multiport --sports 53,123 -m comment --comment "100 forward standard allow udp" -j ACCEPT} ] it 'contains appropriate rules' do run_shell('iptables-save') do |r| @@ -126,13 +125,13 @@ end pp2 = <<-PUPPETCODE - class { '::firewall': } + class { 'firewall': } Firewall { proto => 'all', } Firewallchain { - purge => 'true', + purge => true, ignore => [ '--comment "[^"]*(?i:ignore)[^"]*"', ], @@ -141,33 +140,33 @@ class { '::firewall': } firewall { '001 ssh needed for beaker testing': proto => 'tcp', dport => '22', - action => 'accept', + jump => 'ACCEPT', before => Firewallchain['INPUT:filter:IPv4'], } firewall { '010 INPUT allow established and related': proto => 'all', ctstate => ['ESTABLISHED', 'RELATED'], - action => 'accept', + jump => 'ACCEPT', before => Firewallchain['INPUT:filter:IPv4'], } firewall { "011 reject local traffic not on loopback interface": iniface => '! lo', proto => 'all', - destination => '127.0.0.1/8', - action => 'reject', + destination => '127.0.0.0/8', + jump => 'REJECT', } firewall { '012 accept loopback': iniface => 'lo', - action => 'accept', + jump => 'ACCEPT', before => Firewallchain['INPUT:filter:IPv4'], } firewall { '020 ssh': proto => 'tcp', dport => '22', ctstate => 'NEW', - action => 'accept', + jump => 'ACCEPT', before => Firewallchain['INPUT:filter:IPv4'], } @@ -177,29 +176,29 @@ class { '::firewall': } proto => 'tcp', dport => '25', ctstate => 'NEW', - action => 'accept', + jump => 'ACCEPT', } firewall { '013 icmp echo-request': proto => 'icmp', icmp => 'echo-request', - action => 'accept', + jump => 'ACCEPT', source => '10.0.0.0/8', } firewall { '013 icmp destination-unreachable': proto => 'icmp', icmp => 'destination-unreachable', - action => 'accept', + jump => 'ACCEPT', } firewall { '013 icmp time-exceeded': proto => 'icmp', icmp => 'time-exceeded', - action => 'accept', + jump => 'ACCEPT', } firewall { '443 ssl on aliased interface': proto => 'tcp', dport => '443', ctstate => 'NEW', - action => 'accept', + jump => 'ACCEPT', iniface => 'eth0:3', } @@ -226,7 +225,7 @@ class { '::firewall': } chain => 'FORWARD', proto => 'all', ctstate => ['ESTABLISHED','RELATED'], - action => 'accept', + jump => 'ACCEPT', before => Firewallchain['FORWARD:filter:IPv4'], } firewallchain { 'FORWARD:filter:IPv4': @@ -253,19 +252,19 @@ class { '::firewall': } %r{OUTPUT ACCEPT}, %r{LOCAL_INPUT}, %r{LOCAL_INPUT_PRE}, - %r{-A INPUT -m comment --comment \"001 LOCAL_INPUT_PRE\" -j LOCAL_INPUT_PRE}, - %r{-A INPUT -p tcp -m multiport --dports 22 -m comment --comment \"001 ssh needed for beaker testing\" -j ACCEPT}, - %r{-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment \"010 INPUT allow established and related\" -j ACCEPT}, - %r{-A INPUT -d 127.0.0.0\/(8|255\.0\.0\.0) (! -i|-i !) lo -m comment --comment \"011 reject local traffic not on loopback interface\" -j REJECT --reject-with icmp-port-unreachable}, - %r{-A INPUT -i lo -m comment --comment \"012 accept loopback\" -j ACCEPT}, - %r{-A INPUT -p icmp -m icmp --icmp-type 3 -m comment --comment \"013 icmp destination-unreachable\" -j ACCEPT}, - %r{-A INPUT -s 10.0.0.0\/(8|255\.0\.0\.0) -p icmp -m icmp --icmp-type 8 -m comment --comment \"013 icmp echo-request\" -j ACCEPT}, - %r{-A INPUT -p icmp -m icmp --icmp-type 11 -m comment --comment \"013 icmp time-exceeded\" -j ACCEPT}, - %r{-A INPUT -p tcp -m multiport --dports 22 -m conntrack --ctstate NEW -m comment --comment \"020 ssh\" -j ACCEPT}, - %r{-A INPUT -i eth0:3 -p tcp -m multiport --dports 443 -m conntrack --ctstate NEW -m comment --comment \"443 ssl on aliased interface\" -j ACCEPT}, - %r{-A INPUT -m comment --comment \"900 LOCAL_INPUT\" -j LOCAL_INPUT}, - %r{-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment \"010 allow established and related\" -j ACCEPT}, - %r{-A OUTPUT (! -o|-o !) eth0:2 -p tcp -m multiport --dports 25 -m conntrack --ctstate NEW -m comment --comment \"025 smtp\" -j ACCEPT}, + %r{-A INPUT -m comment --comment "001 LOCAL_INPUT_PRE" -j LOCAL_INPUT_PRE}, + %r{-A INPUT -p (tcp|6) -m tcp --dport 22 -m comment --comment "001 ssh needed for beaker testing" -j ACCEPT}, + %r{-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment "010 INPUT allow established and related" -j ACCEPT}, + %r{-A INPUT -d 127.0.0.0/(8|255\.0\.0\.0) (! -i|-i !) lo -m comment --comment "011 reject local traffic not on loopback interface" -j REJECT --reject-with icmp-port-unreachable}, + %r{-A INPUT -i lo -m comment --comment "012 accept loopback" -j ACCEPT}, + %r{-A INPUT -p (icmp|1) -m icmp --icmp-type 3 -m comment --comment "013 icmp destination-unreachable" -j ACCEPT}, + %r{-A INPUT -s 10.0.0.0/(8|255\.0\.0\.0) -p (icmp|1) -m icmp --icmp-type 8 -m comment --comment "013 icmp echo-request" -j ACCEPT}, + %r{-A INPUT -p (icmp|1) -m icmp --icmp-type 11 -m comment --comment "013 icmp time-exceeded" -j ACCEPT}, + %r{-A INPUT -p (tcp|6) -m tcp --dport 22 -m conntrack --ctstate NEW -m comment --comment "020 ssh" -j ACCEPT}, + %r{-A INPUT -i eth0:3 -p (tcp|6) -m tcp --dport 443 -m conntrack --ctstate NEW -m comment --comment "443 ssl on aliased interface" -j ACCEPT}, + %r{-A INPUT -m comment --comment "900 LOCAL_INPUT" -j LOCAL_INPUT}, + %r{-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment "010 allow established and related" -j ACCEPT}, + %r{-A OUTPUT (! -o|-o !) eth0:2 -p (tcp|6) -m tcp --dport 25 -m conntrack --ctstate NEW -m comment --comment "025 smtp" -j ACCEPT}, ] it 'contains appropriate rules' do run_shell('iptables-save') do |r| diff --git a/spec/acceptance/standard_usage_spec.rb b/spec/acceptance/standard_usage_spec.rb index ccb787d7b..2511a9193 100644 --- a/spec/acceptance/standard_usage_spec.rb +++ b/spec/acceptance/standard_usage_spec.rb @@ -13,28 +13,28 @@ class my_fw::pre { # Default firewall rules firewall { '000 accept all icmp': proto => 'icmp', - action => 'accept', + jump => 'ACCEPT', }-> firewall { '001 accept all to lo interface': proto => 'all', iniface => 'lo', - action => 'accept', + jump => 'ACCEPT', }-> firewall { "0002 reject local traffic not on loopback interface": iniface => '! lo', - destination => '127.0.0.1/8', - action => 'reject', + destination => '127.0.0.0/8', + jump => 'REJECT', }-> firewall { '003 accept related established rules': proto => 'all', ctstate => ['RELATED', 'ESTABLISHED'], - action => 'accept', + jump => 'ACCEPT', } } class my_fw::post { firewall { '999 drop all': proto => 'all', - action => 'drop', + jump => 'DROP', before => undef, } } @@ -48,9 +48,9 @@ class my_fw::post { class { ['my_fw::pre', 'my_fw::post']: } class { 'firewall': } firewall { '500 open up port 22': - action => 'accept', + jump => 'ACCEPT', proto => 'tcp', - dport => 22, + dport => '22', } PUPPETCODE it 'applies twice' do diff --git a/spec/spec_helper_acceptance_local.rb b/spec/spec_helper_acceptance_local.rb index 8027ba0d4..3f8295a90 100644 --- a/spec/spec_helper_acceptance_local.rb +++ b/spec/spec_helper_acceptance_local.rb @@ -99,10 +99,6 @@ def fetch_os_name LitmusHelper.instance.run_shell('update-alternatives --set iptables /usr/sbin/iptables-legacy', expect_failures: true) LitmusHelper.instance.run_shell('update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy', expect_failures: true) end - if ['oraclelinux-6', 'scientific-6'].include?("#{fetch_os_name}-#{os[:release].to_i}") - pp = "class { 'firewall': ensure => stopped }" - LitmusHelper.instance.apply_manifest(pp) - end pp = <<-PUPPETCODE package { 'conntrack-tools': ensure => 'latest', diff --git a/spec/unit/puppet/provider/firewall/firewall_private_get_spec.rb b/spec/unit/puppet/provider/firewall/firewall_private_get_spec.rb new file mode 100644 index 000000000..8f1bce61f --- /dev/null +++ b/spec/unit/puppet/provider/firewall/firewall_private_get_spec.rb @@ -0,0 +1,415 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/resource_api' + +ensure_module_defined('Puppet::Provider::Firewall') +require 'puppet/provider/firewall/firewall' + +RSpec.describe Puppet::Provider::Firewall::Firewall do + describe 'Private Methods - Get' do + subject(:provider) { described_class } + + let(:type) { Puppet::Type.type('firewall') } + let(:context) { Puppet::ResourceApi::BaseContext.new(type.type_definition.definition) } + + # describe "self.get_rules(context, basic, protocols = ['IPv4', 'IPv6'])" do + # No tests written as method is mainly a wrapper and contains little to no actual logic. + # end + + describe 'self.rule_to_name(_context, rule, table_name, protocol)' do + [ + { + rule: '-A INPUT -p tcp -m comment --comment "001 test rule"', + table_name: 'filter', protocol: 'IPv4', + result: { ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT' } + }, + { + rule: '-A OUTPUT -p tcp -m comment --comment "002 test rule"', + table_name: 'raw', protocol: 'IPv6', + result: { ensure: 'present', table: 'raw', protocol: 'IPv6', name: '002 test rule', chain: 'OUTPUT' } + }, + ].each do |test| + it "parses the rule: '#{test[:rule]}'" do + expect(provider.rule_to_name(context, test[:rule], test[:table_name], test[:protocol])).to eq(test[:result]) + end + end + end + + describe 'self.rule_to_hash(_context, rule, table_name, protocol)' do + # Since the boolean values are returned at all times, we keep them as a seperate hash and then merge the situational + # expected results into them, overwriting these ones if needed. + let(:boolean_block) do + { + checksum_fill: false, clamp_mss_to_pmtu: false, isfragment: false, ishasmorefrags: false, islastfrag: false, + isfirstfrag: false, log_uid: false, log_tcp_sequence: false, log_tcp_options: false, log_ip_options: false, + random_fully: false, random: false, rdest: false, reap: false, rsource: false, rttl: false, socket: false, + physdev_is_bridged: false, physdev_is_in: false, physdev_is_out: false, time_contiguous: false, + kernel_timezone: false, clusterip_new: false, queue_bypass: false, ipvs: false, notrack: false + } + end + + [ + { + logic_section: ':name, :string, :string_hex, :bytecode, :u32, :nflog_prefix, :log_prefix', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule"', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule"', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" --string test_string', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" --string test_string', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + string: 'test_string' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" ! --string test_string', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" ! --string test_string', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + string: '! test_string' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" ! --string "test string"', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" ! --string "test string"', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + string: '! test string' + } }, + ] + }, + { + logic_section: ':sport, :dport', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" --dport 20', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" --dport 20', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + dport: '20' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" --dport 20:30', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" --dport 20:30', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + dport: '20:30' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" ! --dport 20', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" ! --dport 20', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + dport: '! 20' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m multiport --dports 20,30,40:50', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m multiport --dports 20,30,40:50', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + dport: ['20', '30', '40:50'] + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m multiport ! --dports 20,30,40:50', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m multiport ! --dports 20,30,40:50', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + dport: ['! 20', '30', '40:50'] + } }, + ] + }, + { + logic_section: ':tcp_flags', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" --tcp-flags FIN,SYN,RST FIN', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" --tcp-flags FIN,SYN,RST FIN', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + tcp_flags: 'FIN,SYN,RST FIN' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" ! --tcp-flags FIN,SYN,RST FIN', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" ! --tcp-flags FIN,SYN,RST FIN', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + tcp_flags: '! FIN,SYN,RST FIN' + } }, + ] + }, + { + logic_section: ':src_type, :dst_type, :ipset, :match_mark, :mss, :connmark', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m addrtype --src-type LOCAL', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m addrtype --src-type LOCAL', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + src_type: 'LOCAL' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m addrtype --src-type LOCAL --limit-iface-in -m addrtype ! --src-type UNICAST', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m addrtype --src-type LOCAL --limit-iface-in -m addrtype ! --src-type UNICAST', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + src_type: ['LOCAL --limit-iface-in', '! UNICAST'] + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m set --match-set denylist src,dst', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m set --match-set denylist src,dst', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + ipset: 'denylist src,dst' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m set --match-set denylist dst -m set ! --match-set denylist2 src,dst', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m set --match-set denylist dst -m set ! --match-set denylist2 src,dst', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + ipset: ['denylist dst', '! denylist2 src,dst'] + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m mark --mark 0x1', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m mark --mark 0x1', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + match_mark: '0x1' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m mark --mark 0x32 -m mark ! --mark 0x1', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m mark --mark 0x32 -m mark ! --mark 0x1', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + match_mark: ['0x32', '! 0x1'] + } }, + ] + }, + { + logic_section: ':state, :ctstate, :ctstatus, :month_days, :week_days', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -state --state INVALID', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -state --state INVALID', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + state: 'INVALID' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -state ! --state INVALID', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -state ! --state INVALID', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + state: '! INVALID' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -state ! --state INVALID,NEW', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -state ! --state INVALID,NEW', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + state: ['! INVALID', 'NEW'] + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" --monthdays 26,28', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" --monthdays 26,28', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + month_days: [26, 28] + } }, + ] + }, + { + logic_section: ':icmp', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m icmp --icmp-type 0', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m icmp --icmp-type 0', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + icmp: '0' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m icmp6 --icmpv6-type 129', + table_name: 'filter', protocol: 'IPv6', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m icmp6 --icmpv6-type 129', + ensure: 'present', table: 'filter', protocol: 'IPv6', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + icmp: '129' + } }, + ] + }, + { + logic_section: ':recent', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m recent --set', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m recent --set', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + recent: 'set' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m recent ! --set', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m recent ! --set', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + recent: '! set' + } }, + ] + }, + { + logic_section: ':rpfilter', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m rpfilter --loose', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m rpfilter --loose', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + rpfilter: 'loose' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m rpfilter --loose -m rpfilter --accept-local', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m rpfilter --loose -m rpfilter --accept-local', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + rpfilter: ['loose', 'accept-local'] + } }, + ] + }, + { + logic_section: ':proto, :source, :destination, :iniface, :outiface, etc.', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -s 192.168.2.0/24', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -s 192.168.2.0/24', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + source: '192.168.2.0/24' + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" ! -s 192.168.2.0/24', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" ! -s 192.168.2.0/24', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + source: '! 192.168.2.0/24' + } }, + ] + }, + { + logic_section: 'Default (i.e. :chain, stat_mode, stat_every, stat_packet, etc.)', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -m statistic --mode nth', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -m statistic --mode nth', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + stat_mode: 'nth' + } }, + ] + }, + { + logic_section: 'Boolean (i.e. :checksum_fill, :clamp_mss_to_pmtu, :isfragment, etc)', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" --checksum-fill', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" --checksum-fill', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + checksum_fill: true + } }, + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" --checksum-fill -m socket', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" --checksum-fill -m socket', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + checksum_fill: true, socket: true + } }, + ] + }, + { + logic_section: 'non-complex flags (i.e. :isfragment = `-f`)', rules: [ + { rule: '-A INPUT -p tcp -m comment --comment "001 test rule" -f --tcp-flags FIN,SYN,RST,ACK SYN', + table_name: 'filter', protocol: 'IPv4', + result: { + line: '-A INPUT -p tcp -m comment --comment "001 test rule" -f --tcp-flags FIN,SYN,RST,ACK SYN', + ensure: 'present', table: 'filter', protocol: 'IPv4', name: '001 test rule', chain: 'INPUT', proto: 'tcp', + isfragment: true, tcp_flags: 'FIN,SYN,RST,ACK SYN' + } }, + ] + }, + ].each do |test| + context "with logic section: #{test[:logic_section]}" do + test[:rules].each do |rule| + it "parses the rule: '#{rule[:rule]}'" do + # `.sort` added to each value for conveniance sake + expect(provider.rule_to_hash(context, rule[:rule], rule[:table_name], rule[:protocol]).sort) + .to eq(boolean_block.merge(rule[:result]).sort) + end + end + end + end + end + + describe 'self.validate_get(_context, rules)' do + [ + { + input: [{ name: '001 test rule' }, { name: '001 test rule' }], + error: 'Duplicate names have been found within your Firewalls. This prevents the module from working correctly and must be manually resolved.' + }, + ].each do |validate| + it "Validate: #{validate[:error]}" do + expect { provider.validate_get(context, validate[:input]) }.to raise_error(ArgumentError, validate[:error]) + end + end + end + + describe 'self.process_get(_context, rule_hash, rule, counter)' do + [ + { + process: 'if no `name` is returned, generate one', + rule_hash: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: 'tcp' }, + rule: 'A INPUT -p tcp', + counter: 1, + result: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: 'tcp', + name: '9001 9115c4f4e9a27e6519767ade2f6aa22a66268cea6bf3fb739c6963cdbadcf682' } + }, + { + process: 'if returned `name` has no number, assign one', + rule_hash: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: 'tcp', name: 'test rule' }, + rule: 'A INPUT -p tcp -m comment --comment "test rule"', + counter: 2, + result: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: 'tcp', + name: '9002 test rule' } + }, + { + process: 'if no `proto` is returned, assume it is `all`', + rule_hash: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', name: '001 test rule' }, + rule: 'A INPUT -m comment --comment "001 test rule"', + counter: 0, + result: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: 'all', name: '001 test rule' } + }, + { + process: 'if `proto` is returned as number, convert to name', + rule_hash: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: '1', name: '001 test rule' }, + rule: 'A INPUT -p 1 -m comment --comment "001 test rule"', + counter: 0, + result: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: 'icmp', name: '001 test rule' } + }, + { + process: "if `set_dscp` is returned, also return it's valid class name `set_dscp_class`", + rule_hash: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: 'tcp', + name: '001 test rule', set_dscp: '0x0c' }, + rule: 'A INPUT -p tcp -m comment --comment "001 test rule" --set-dscp 0x0c', + counter: 0, + result: { ensure: 'present', table: 'filter', protocol: 'IPv4', chain: 'INPUT', proto: 'tcp', + name: '001 test rule', set_dscp: '0x0c', set_dscp_class: 'af12' } + }, + ].each do |process| + it "Process: #{process[:process]}" do + expect(provider.process_get(context, process[:rule_hash], process[:rule], process[:counter])).to eq(process[:result]) + end + end + end + + describe 'self.create_absent(namevar, title)' do + [ + { namevar: :name, title: '001 test rule', result: { ensure: 'absent', name: '001 test rule' } }, + ].each do |create| + it "Create: #{create}" do + expect(provider.create_absent(create[:namevar], create[:title])).to eq(create[:result]) + end + end + end + end +end diff --git a/spec/unit/puppet/provider/firewall/firewall_private_set_spec.rb b/spec/unit/puppet/provider/firewall/firewall_private_set_spec.rb new file mode 100644 index 000000000..b480a8317 --- /dev/null +++ b/spec/unit/puppet/provider/firewall/firewall_private_set_spec.rb @@ -0,0 +1,491 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/resource_api' + +ensure_module_defined('Puppet::Provider::Firewall') +require 'puppet/provider/firewall/firewall' + +RSpec.describe Puppet::Provider::Firewall::Firewall do + describe 'Private Methods - Set' do + subject(:provider) { described_class } + + let(:type) { Puppet::Type.type('firewall') } + let(:context) { Puppet::ResourceApi::BaseContext.new(type.type_definition.definition) } + + describe 'self.validate_input(_is, should)' do + [ + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule' } }, + invalid: { is: {}, should: { ensure: 'present', name: '9001 Test Rule' } }, + error: 'Rule name cannot start with 9000-9999, as this range is reserved for unmanaged rules.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', proto: 'tcp', isfragment: true } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', proto: 'all', isfragment: true } }, + error: '`proto` must be set to `tcp` for `isfragment` to be true.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', stat_mode: 'nth', stat_every: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', stat_mode: 'random', stat_every: 313 } }, + error: '`stat_mode` must be set to `nth` for `stat_every` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', stat_mode: 'nth', stat_packet: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', stat_packet: 313 } }, + error: '`stat_mode` must be set to `nth` for `stat_packet` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', stat_mode: 'random', stat_probability: 0.5 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', stat_mode: 'nth', stat_probability: 0.5 } }, + error: '`stat_mode` must be set to `random` for `stat_probability` to be set.' + }, + { + # Covers `dport`, `sport`, `state`, `ctstate` and `ctstatus` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', dport: ['! 54', '64', '74'] } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', dport: ['54', '! 64', '74'] } }, + error: 'When negating a `dport` array, you must negate either the first given value only or all the given values.' + }, + { + # Covers `dport`, `sport`, `state`, `ctstate` and `ctstatus` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', sport: ['! 54', '! 64', '! 74'] } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', sport: ['! 54', '! 64', '74'] } }, + error: 'When negating a `sport` array, you must negate either the first given value only or all the given values.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', icmp: 'any' } }, + error: 'Value `any` is not valid. This behaviour should be achieved by omitting or undefining the ICMP parameter.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', limit: '50/sec', burst: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', burst: 313 } }, + error: '`burst` cannot be set without `limit`.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', length: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', length: 65_536 } }, + error: '`length` values must be between 0 and 65535' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'rcheck', rseconds: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'remove', rseconds: 313 } }, + error: '`recent` must be set to `update` or `rcheck` for `rseconds` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'rcheck', rseconds: 313, reap: true } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'rcheck', reap: true } }, + error: '`rseconds` must be set for `reap` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'rcheck', rhitcount: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'remove', rhitcount: 313 } }, + error: '`recent` must be set to `update` or `rcheck` for `rhitcount` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'update', rttl: true } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'remove', rttl: true } }, + error: '`recent` must be set to `update` or `rcheck` for `rttl` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'rcheck', rname: 'test' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', rname: 'test' } }, + error: '`recent` must be set for `rname` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'rcheck', rsource: 'test' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', rsource: 'test' } }, + error: '`recent` must be set for `rsource` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'rcheck', rdest: 'test' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', rdest: 'test' } }, + error: '`recent` must be set for `rdest` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'update', rsource: 'test' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', recent: 'update', rsource: 'test', rdest: 'test' } }, + error: '`rdest` and `rsource` are mutually exclusive, only one may be set at a time.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', string_algo: 'bm', string_hex: 'test', string: 'test' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', string_hex: 'test', string: 'test' } }, + error: '`string_algo` must be set for `string` or `string_hex` to be set.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', queue_num: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', queue_num: 65_536 } }, + error: '`queue_num`` must be between 0 and 65535' + }, + { + # `2^16-1` is equal to `65_535` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', nflog_group: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', nflog_group: 65_536 } }, + error: '`nflog_group` must be between 0 and 2^16-1' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'TEE', gateway: '0.0.0.0' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'TEE' } }, + error: 'When setting `jump => TEE`, the gateway property is required' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'TCPMSS', set_mss: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'TCPMSS' } }, + error: 'When setting `jump => TCPMSS`, the `set_mss` or `clamp_mss_to_pmtu` property is required' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'DSCP', set_dscp_class: 'af11' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'DSCP' } }, + error: 'When setting `jump => DSCP`, the `set_dscp` or `set_dscp_class` property is required' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'DNAT', todest: '0.0.0.0', table: 'nat' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'DNAT', todest: '0.0.0.0', table: 'filter' } }, + error: 'Parameter `jump => DNAT` only applies to `table => nat`' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'DNAT', table: 'nat', todest: '0.0.0.0' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'DNAT', table: 'nat' } }, + error: 'Parameter `jump => DNAT` must have `todest` parameter' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'SNAT', tosource: '0.0.0.0', table: 'nat' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'SNAT', tosource: '0.0.0.0', table: 'filter' } }, + error: 'Parameter `jump => SNAT` only applies to `table => nat`' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'SNAT', table: 'nat', tosource: '0.0.0.0' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'SNAT', table: 'nat' } }, + error: 'Parameter `jump => SNAT` must have `tosource` parameter' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'CHECKSUM', table: 'mangle', checksum_fill: true } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', table: 'filter', checksum_fill: true } }, + error: 'Parameter `checksum_fill` requires `jump => CHECKSUM` and `table => mangle`' + }, + { + # Covers `log_prefix`, `log_level`, `log_uid`, `log_tcp_sequence`, `log_tcp_options` and `log_ip_options` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'LOG', log_prefix: 'Test prefix' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', jump: 'CHECKSUM', log_prefix: 'Test prefix' } }, + error: 'Parameter `log_prefix` requires `jump => LOG`' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', table: 'raw', jump: 'CT' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', table: 'filter', jump: 'CT' } }, + error: 'Parameter `jump => CT` only applies to `table => raw`' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test`` Rule', table: 'raw', jump: 'CT', zone: 313 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', table: 'filter', zone: 313 } }, + error: 'Parameter `zone` requires `jump => CT`' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test`` Rule', table: 'raw', jump: 'CT', helper: 'helperOne' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', table: 'filter', helper: 'helperOne' } }, + error: 'Parameter `helper` requires `jump => CT`' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test`` Rule', table: 'raw', jump: 'CT', notrack: true } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', table: 'filter', notrack: true } }, + error: 'Parameter `notrack` requires `jump => CT`' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test`` Rule', connlimit_mask: 32, connlimit_upto: 5 } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', connlimit_mask: 32 } }, + error: 'Parameter `connlimit_mask` requires either `connlimit_upto` or `connlimit_above`' + }, + { + # Covers `hashlimit_upto`, `hashlimit_above`, `hashlimit_name`, `hashlimit_burst`, `hashlimit_mode`, `hashlimit_srcmask`, + # `hashlimit_dstmask`, `hashlimit_htable_size`, `hashlimit_htable_max`, `hashlimit_htable_expire` and `hashlimit_htable_gcinterval` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', hashlimit_name: 'hashlimit test', hashlimit_upto: '40/min' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', hashlimit_upto: '40/min' } }, + error: 'Parameter `hashlimit_name` and either `hashlimit_upto` or `hashlimit_above` are required when setting any `hashlimit` attribute.' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', hashlimit_name: 'hashlimit test', hashlimit_upto: '40/min' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', hashlimit_name: 'hashlimit test', hashlimit_upto: '40/min', hashlimit_above: '40/min' } }, + error: '`hashlimit_upto` and `hashlimit_above` are mutually exclusive, only one may be set at a time.' + }, + { + # Covers `clusterip_new`, `clusterip_hashmode`, `clusterip_clustermac`, `clusterip_total_nodes`, `clusterip_local_node`, `clusterip_hash_init` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', protocol: 'IPv4', clusterip_new: true } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', protocol: 'IPv6', clusterip_new: true } }, + error: 'Parameter `clusterip_new` is specific to the `IPv4` protocol' + }, + { + # Covers `hop_limit`, `ishasmorefrags`, `islastfrag`, `isfirstfrag` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', protocol: 'ip6tables', islastfrag: true } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', protocol: 'iptables', islastfrag: true } }, + error: 'Parameter `islastfrag` is specific to the `IPv6` protocol' + }, + { + # Covers `dst_type` and `src_type` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', dst_type: ['! LOCAL', '! UNICAST'] } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', dst_type: ['LOCAL', 'LOCAL'] } }, + error: '`dst_type` elements must be unique' + }, + { + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', nflog_prefix: 'Test prefix' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', nflog_prefix: '12345678901234567890123456789012345678901234567890123456789012345' } }, + error: 'Parameter `nflog_prefix`` must be less than 64 characters' + }, + { + # Covers `dst_range` and `src_range` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', protocol: 'IPv4', dst_range: '192.168.1.1-192.168.1.10' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', protocol: 'IPv4', dst_range: '5.10.64.0/24' } }, + error: 'The IP range must be in `IP1-IP2` format.' + }, + { + # Covers `dst_range` and `src_range` + valid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', protocol: 'IPv6', src_range: '1::46-2::48' } }, + invalid: { is: {}, should: { ensure: 'present', name: '001 Test Rule', protocol: 'IPv6', src_range: '1::46-248' } }, + error: 'Invalid IP address `248` in range `1::46-248`' + }, + ].each do |validate| + context "when validating: #{validate[:error]}" do + it "valid Input: #{validate[:valid][:should]}" do + expect { provider.validate_input(validate[:valid][:is], validate[:valid][:should]) }.not_to raise_error + end + + it "invalid Input: #{validate[:invalid][:should]}" do + expect { provider.validate_input(validate[:invalid][:is], validate[:invalid][:should]) }.to raise_error(ArgumentError, validate[:error]) + end + end + end + end + + describe 'self.process_input(should)' do + [ + { + process: '`dport`, `sport` `state` `ctstate` and `ctstatus` arrays should only have the first value negated', + should: { dport: ['! 54', '! 64', '! 74'] }, + result: { dport: ['! 54', '64', '74'] } + }, + { + process: '`jump` values should always be uppercase', + should: { jump: 'accept' }, + result: { jump: 'ACCEPT' } + }, + { + process: '`source` and `destination` must be put through host_to_mask with protocol `IPv4`', + should: { protocol: 'IPv4', source: '! 96.126.112.51', destination: '96.126.112.51' }, + result: { protocol: 'IPv4', source: '! 96.126.112.51/32', destination: '96.126.112.51/32' } + }, + { + process: '`source` and `destination` must be put through host_to_mask with protocol `IPv6`', + should: { protocol: 'IPv6', source: '2001:db8:1234::', destination: '! 2001:db8:1234::' }, + result: { protocol: 'IPv6', source: '2001:db8:1234::/128', destination: '! 2001:db8:1234::/128' } + }, + { + process: '`ct` attributes must be put through a restricted host_to_mask with protocol `IPv4`', + should: { protocol: 'IPv4', ctorigsrc: '96.126.112.51/32', ctorigdst: '! 96.126.112.51', ctreplsrc: '96.126.112.51/24', ctrepldst: '96.126.112.51/16' }, + result: { protocol: 'IPv4', ctorigsrc: '96.126.112.51', ctorigdst: '! 96.126.112.51', ctreplsrc: '96.126.112.0/24', ctrepldst: '96.126.0.0/16' } + }, + { + process: '`ct` attributes must be put through a restricted host_to_mask with protocol `IPv6`', + should: { protocol: 'IPv6', ctorigsrc: '! 2001:db8:1234::', ctorigdst: '2001:db8:1234::/128', ctreplsrc: '2001:db8:1234::/32', ctrepldst: '2001:db8:1234::/16' }, + result: { protocol: 'IPv6', ctorigsrc: '! 2001:db8:1234::', ctorigdst: '2001:db8:1234::', ctreplsrc: '2001:db8::/32', ctrepldst: '2001::/16' } + }, + { + process: '`icmp` needs to be converted to a number if passed as a string with protocol `IPv4`', + should: { protocol: 'IPv4', icmp: 'destination-unreachable' }, + result: { protocol: 'IPv4', icmp: '3' } + }, + { + process: '`icmp` needs to be converted to a number if passed as a string with protocol `IPv6`', + should: { protocol: 'IPv6', icmp: 'destination-unreachable' }, + result: { protocol: 'IPv6', icmp: '1' } + }, + { + process: '`log_level` needs to be converted to a number if passed as a string', + should: { log_level: 'alert' }, + result: { log_level: '1' } + }, + { + process: '`set_mark`, `match_mark` and `connmark` must be put through mark_mask_to_hex/mark_to_hex', + should: { set_mark: '42', match_mark: '42', connmark: '42' }, + result: { set_mark: '0x2a/0xffffffff', match_mark: '0x2a', connmark: '0x2a' } + }, + { + process: '`time_start` and `time_stop` must be applied in full HH:MM:SS format', + should: { time_start: '9:30', time_stop: '12:45' }, + result: { time_start: '09:30:00', time_stop: '12:45:00' } + }, + { + process: 'If `sport` or `dport` has been pass arange with `-` as the divider, replace it with `:`', + should: { sport: '50-60', dport: ['50-60', '70-80'] }, + result: { sport: '50:60', dport: ['50:60', '70:80'] } + }, + ].each do |process| + it "Process: #{process[:process]}" do + expect(provider.process_input(process[:should])).to eq(process[:result]) + end + end + end + + describe 'self.hash_to_rule(_context, _name, rule)' do + [ + { + logic_section: ':name, :string, :string_hex, :bytecode, :u32, :nflog_prefix, :log_prefix', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule' }, + result: " -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', string_hex: 'test' }, + result: " -m string --hex-string 'test' -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', string_hex: '! test' }, + result: " -m string ! --hex-string 'test' -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: ':sport, :dport', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', sport: '50', dport: '! 60' }, + result: " --sport 50 ! --dport 60 -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', sport: '50:60', dport: '! 60:70' }, + result: " --sport 50:60 ! --dport 60:70 -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', sport: ['50', '60:70'], dport: ['! 80:90', '100'] }, + result: " -m multiport --sports 50,60:70 -m multiport ! --dports 80:90,100 -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: ':src_type, :dst_type, :ipset, :match_mark, :mss, :connmark', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', src_type: 'LOCAL --limit-iface-in', dst_type: '! UNICAST' }, + result: " -m addrtype --src-type LOCAL --limit-iface-in -m addrtype ! --dst-type UNICAST -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', src_type: ['LOCAL', '! UNICAST'], dst_type: ['! UNICAST', 'LOCAL'] }, + result: " -m addrtype --src-type LOCAL -m addrtype ! --src-type UNICAST -m addrtype ! --dst-type UNICAST -m addrtype --dst-type LOCAL -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: ':state, :ctstate, :ctstatus, :month_days, :week_days', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', state: 'INVALID', ctstate: '! INVALID', month_days: 22 }, + result: " -m state --state INVALID -m conntrack ! --ctstate INVALID -m time --monthdays 22 -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', state: ['INVALID', 'ESTABLISHED'], ctstate: ['! INVALID', 'ESTABLISHED'], month_days: [22, 24, 30] }, + result: " -m state --state INVALID,ESTABLISHED -m conntrack ! --ctstate INVALID,ESTABLISHED -m time --monthdays 22,24,30 -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: ':icmp', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', protocol: 'IPv4', icmp: '3' }, + result: " -m icmp --icmp-type 3 -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', protocol: 'IPv6', icmp: '3' }, + result: " -m icmp6 --icmpv6-type 3 -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', protocol: 'IPv4', icmp: '! 3' }, + result: " -m icmp ! --icmp-type 3 -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', protocol: 'IPv6', icmp: '! 3' }, + result: " -m icmp6 ! --icmpv6-type 3 -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: ':recent', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', recent: 'update' }, + result: " -m recent --update -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', recent: '! update' }, + result: " -m recent ! --update -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: ':rpfilter', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', rpfilter: 'loose' }, + result: " -m rpfilter --loose -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', rpfilter: ['loose', 'validmark'] }, + result: " -m rpfilter --loose --validmark -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: ':proto, :source, :destination, :iniface, :outiface, :physdev_in, etc.', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', proto: 'icmp', source: '192.168.2.0/24' }, + result: " -s 192.168.2.0/24 -p icmp -m comment --comment '001 test rule'" }, + { name: '001 test rule', + hash: { name: '001 test rule', proto: '! icmp', source: '! 192.168.2.0/24' }, + result: " ! -s 192.168.2.0/24 ! -p icmp -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: 'Default (i.e.: stat_mode, stat_every, stat_packet, stat_probability, etc.)', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', stat_mode: 'nth', stat_every: 5 }, + result: " -m statistic --mode nth --every 5 -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: 'Boolean (i.e. :checksum_fill, :clamp_mss_to_pmtu, :isfragment, etc)', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', checksum_fill: true, random: true }, + result: " --checksum-fill --random -m comment --comment '001 test rule'" }, + ] + }, + { + logic_section: 'module_to_argument_mapping', rules: [ + { name: '001 test rule', + hash: { name: '001 test rule', physdev_in: 'lo', physdev_out: '! lo', ipsec_dir: 'in', ipsec_policy: 'ipsec' }, + result: " -m physdev --physdev-in lo ! --physdev-out lo -m policy --dir in --pol ipsec -m comment --comment '001 test rule'" }, + ] + }, + ].each do |test| + context "with logic section: #{test[:logic_section]}" do + test[:rules].each do |rule| + it "parses hash: '#{rule[:hash]}'" do + expect(provider.hash_to_rule(context, rule[:name], rule[:hash])).to eq(rule[:result]) + end + end + end + end + end + + describe 'self.insert_order(context, name, chain, table, protocol)' do + let(:ipv4_rules) do + [{ ensure: 'present', table: 'filter', protocol: 'IPv4', name: '9001 test rule', chain: 'INPUT' }, + { ensure: 'present', table: 'filter', protocol: 'IPv4', name: '002 test rule', chain: 'INPUT' }, + { ensure: 'present', table: 'filter', protocol: 'IPv4', name: '003 test rule', chain: 'INPUT' }, + { ensure: 'present', table: 'filter', protocol: 'IPv4', name: '9002 test rule', chain: 'INPUT' }, + { ensure: 'present', table: 'filter', protocol: 'IPv4', name: '9003 test rule', chain: 'INPUT' }, + { ensure: 'present', table: 'filter', protocol: 'IPv4', name: '005 test rule', chain: 'INPUT' }, + { ensure: 'present', table: 'filter', protocol: 'IPv4', name: '006 test rule', chain: 'OUTPUT' }, + { ensure: 'present', table: 'raw', protocol: 'IPv4', name: '007 test rule', chain: 'OUTPUT' }, + { ensure: 'present', table: 'raw', protocol: 'IPv4', name: '008 test rule', chain: 'OUTPUT' }] + end + let(:ipv6_rules) do + [{ ensure: 'present', table: 'filter', protocol: 'IPv6', name: '010 test rule', chain: 'INPUT' }, + { ensure: 'present', table: 'filter', protocol: 'IPv6', name: '012 test rule', chain: 'INPUT' }, + { ensure: 'present', table: 'raw', protocol: 'IPv6', name: '013 test rule', chain: 'OUTPUT' }, + { ensure: 'present', table: 'raw', protocol: 'IPv6', name: '015 test rule', chain: 'OUTPUT' }] + end + + [ + { name: '001 test rule', chain: 'INPUT', table: 'filter', protocol: 'IPv4', result: 1 }, + { name: '002 test rule', chain: 'INPUT', table: 'filter', protocol: 'IPv4', result: 2 }, + { name: '004 test rule', chain: 'INPUT', table: 'filter', protocol: 'IPv4', result: 4 }, + { name: '005 test rule', chain: 'OUTPUT', table: 'filter', protocol: 'IPv4', result: 1 }, + { name: '007 test rule', chain: 'OUTPUT', table: 'raw', protocol: 'IPv4', result: 1 }, + { name: '009 test rule', chain: 'OUTPUT', table: 'raw', protocol: 'IPv4', result: 3 }, + { name: '011 test rule', chain: 'INPUT', table: 'filter', protocol: 'IPv6', result: 2 }, + { name: '016 test rule', chain: 'OUTPUT', table: 'raw', protocol: 'IPv6', result: 3 }, + { name: '017 test rule', chain: 'FORWARD', table: 'filter', protocol: 'IPv4', result: 1 }, + ].each do |rule| + it do + allow(described_class).to receive(:get_rules).with(context, true, ['IPv4']).and_return(ipv4_rules) + allow(described_class).to receive(:get_rules).with(context, true, ['IPv6']).and_return(ipv6_rules) + + expect(provider.insert_order(context, rule[:name], rule[:chain], rule[:table], rule[:protocol])).to eq(rule[:result]) + end + end + end + end +end diff --git a/spec/unit/puppet/provider/firewall/firewall_public_spec.rb b/spec/unit/puppet/provider/firewall/firewall_public_spec.rb new file mode 100644 index 000000000..73a6df861 --- /dev/null +++ b/spec/unit/puppet/provider/firewall/firewall_public_spec.rb @@ -0,0 +1,237 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/resource_api' + +ensure_module_defined('Puppet::Provider::Firewall') +require 'puppet/provider/firewall/firewall' + +RSpec.describe Puppet::Provider::Firewall::Firewall do + describe 'Public Methods' do + subject(:provider) { described_class.new } + + let(:type) { Puppet::Type.type('firewall') } + let(:context) { Puppet::ResourceApi::BaseContext.new(type.type_definition.definition) } + + # describe 'get(_context)' do + # This method is not tested as it simply serves as a wrapper for self.get_rules and self.validate_get and contains no actual logic. + # end + + # describe 'set(context, changes)' do + # This method is not tested as it simply serves as a wrapper with little to no actual logic. + # end + + describe 'create(context, name, should)' do + [ + { + should: { name: '001 IPv4 Test Rule', chain: 'INPUT', table: 'filter', protocol: 'IPv4', ensure: 'present' }, + arguments: '-m comment --comment "001 Test Rule"', + create_command: 'iptables -t filter -I INPUT 1 -m comment --comment "001 Test Rule"' + }, + { + should: { name: '002 IPv6 Test Rule', chain: 'OUTPUT', table: 'raw', protocol: 'IPv6', ensure: 'present' }, + arguments: '-m comment --comment "002 Test Rule"', + create_command: 'ip6tables -t raw -I OUTPUT 1 -m comment --comment "002 Test Rule"' + }, + ].each do |test| + it "creates the resource: '#{test[:should][:name]}'" do + expect(context).to receive(:notice).with(%r{\ACreating Rule '#{test[:should][:name]}'}) + allow(described_class).to receive(:insert_order) + .with(context, test[:should][:name], test[:should][:chain], test[:should][:table], test[:should][:protocol]).and_return(1) + allow(described_class).to receive(:hash_to_rule) + .with(context, test[:should][:name], test[:should]).and_return(test[:arguments]) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:create_command]) + allow(PuppetX::Firewall::Utility).to receive(:persist_iptables).with(context, test[:should][:name], test[:should][:protocol]) + + provider.create(context, test[:should][:name], test[:should]) + end + end + end + + describe 'update(context, name, should, is)' do + [ + { + should: { name: '001 IPv4 Test Rule', chain: 'INPUT', table: 'filter', protocol: 'IPv4', ensure: 'present' }, + arguments: '-m comment --comment "001 Test Rule"', + update_command: 'iptables -t filter -R INPUT 1 -m comment --comment "001 Test Rule"' + }, + { + should: { name: '002 IPv6 Test Rule', chain: 'OUTPUT', table: 'raw', protocol: 'IPv6', ensure: 'present' }, + arguments: '-m comment --comment "002 Test Rule"', + update_command: 'ip6tables -t raw -R OUTPUT 1 -m comment --comment "002 Test Rule"' + }, + ].each do |test| + it "updates the resource: '#{test[:should][:name]}'" do + expect(context).to receive(:notice).with(%r{\Updating Rule '#{test[:should][:name]}'}) + allow(described_class).to receive(:insert_order) + .with(context, test[:should][:name], test[:should][:chain], test[:should][:table], test[:should][:protocol]).and_return(1) + allow(described_class).to receive(:hash_to_rule) + .with(context, test[:should][:name], test[:should]).and_return(test[:arguments]) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:update_command]) + allow(PuppetX::Firewall::Utility).to receive(:persist_iptables).with(context, test[:should][:name], test[:should][:protocol]) + + provider.update(context, test[:should][:name], test[:should]) + end + end + end + + describe 'delete(context, name, is)' do + [ + { + is: { + name: '001 IPv4 Test Rule', chain: 'INPUT', table: 'filter', protocol: 'IPv4', ensure: 'present', + line: '-A INPUT 1 -m comment --comment "001 Test Rule"' + }, + delete_command: 'iptables -t filter -D INPUT 1 -m comment --comment "001 Test Rule"' + }, + { + is: { + name: '002 IPv6 Test Rule', chain: 'OUTPUT', table: 'raw', protocol: 'IPv6', ensure: 'present', + line: '-A OUTPUT 1 -m comment --comment "002 Test Rule"' + }, + delete_command: 'ip6tables -t raw -D OUTPUT 1 -m comment --comment "002 Test Rule"' + }, + ].each do |test| + it "deletes the resource: '#{test[:is][:name]}'" do + allow(context).to receive(:notice).with(%r{\ADeleting Rule '#{test[:is][:name]}'}) + allow(described_class).to receive(:insert_order) + .with(context, test[:is][:name], test[:is][:chain], test[:is][:table], test[:is][:protocol]).and_return(1) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:delete_command]) + allow(PuppetX::Firewall::Utility).to receive(:persist_iptables).with(context, test[:is][:name], test[:is][:protocol]) + + provider.delete(context, test[:is][:name], test[:is]) + end + end + end + + describe 'insync?(context, _name, property_name, _is_hash, _should_hash)' do + [ + { testing: 'protocol', property_name: :protocol, comparisons: [ + { is_hash: { protocol: 'IPv4' }, should_hash: { protocol: 'IPv4' }, result: true }, + { is_hash: { protocol: 'IPv4' }, should_hash: { protocol: 'iptables' }, result: true }, + { is_hash: { protocol: 'IPv4' }, should_hash: { protocol: 'ip6tables' }, result: false }, + { is_hash: { protocol: 'IPv6' }, should_hash: { protocol: 'IPv6' }, result: true }, + { is_hash: { protocol: 'IPv6' }, should_hash: { protocol: 'ip6tables' }, result: true }, + { is_hash: { protocol: 'IPv6' }, should_hash: { protocol: 'iptables' }, result: false }, + ] }, + { testing: 'source/destination', property_name: :source, comparisons: [ + { is_hash: { source: '10.1.5.28/32' }, should_hash: { protocol: 'IPv4', source: '10.1.5.28/32' }, result: true }, + { is_hash: { source: '10.1.5.28/32' }, should_hash: { protocol: 'IPv4', source: '10.1.5.28' }, result: true }, + { is_hash: { source: '10.1.5.28/32' }, should_hash: { protocol: 'iptables', source: '10.1.5.28' }, result: true }, + { is_hash: { source: '10.1.5.28/32' }, should_hash: { protocol: 'iptables', source: '10.1.5.27' }, result: false }, + { is_hash: { source: '1::49/128' }, should_hash: { protocol: 'IPv6', source: '1::49/128' }, result: true }, + { is_hash: { source: '1::49/128' }, should_hash: { protocol: 'IPv6', source: '1::49' }, result: true }, + { is_hash: { source: '1::49/128' }, should_hash: { protocol: 'ip6tables', source: '1::49' }, result: true }, + { is_hash: { source: '1::49/128' }, should_hash: { protocol: 'ip6tables', source: '1::50' }, result: false }, + ] }, + { testing: 'tcp_option/ct_proto/hop_limit', property_name: :tcp_option, comparisons: [ + { is_hash: { tcp_option: '6' }, should_hash: { tcp_option: 6 }, result: true }, + { is_hash: { tcp_option: '6' }, should_hash: { tcp_option: '6' }, result: true }, + { is_hash: { tcp_option: '5' }, should_hash: { tcp_option: '6' }, result: false }, + ] }, + { testing: 'tcp_flags', property_name: :tcp_flags, comparisons: [ + { is_hash: { tcp_flags: 'FIN SYN' }, should_hash: { tcp_flags: 'FIN SYN' }, result: true }, + { is_hash: { tcp_flags: 'FIN,SYN,RST,PSH,ACK,URG FIN,SYN,RST,PSH,ACK,URG' }, should_hash: { tcp_flags: 'ALL ALL' }, result: true }, + { is_hash: { tcp_flags: 'ALL FIN,SYN,RST,PSH,ACK,URG' }, should_hash: { tcp_flags: 'FIN,SYN,RST,PSH,ACK,URG ALL' }, result: true }, + { is_hash: { tcp_flags: 'ALL ALL' }, should_hash: { tcp_flags: 'FIN,SYN,RST,PSH,ACK,URG FIN,SYN,RST,PSH,ACK,URG' }, result: true }, + { is_hash: { tcp_flags: 'FIN SYN' }, should_hash: { tcp_flags: 'SYN FIN' }, result: false }, + ] }, + { testing: 'uid/gid', property_name: :uid, comparisons: [ + { is_hash: { uid: '0' }, should_hash: { uid: 'root' }, result: true }, + { is_hash: { uid: 'root' }, should_hash: { uid: '0' }, result: true }, + { is_hash: { uid: '0' }, should_hash: { uid: '0' }, result: true }, + { is_hash: { uid: 'root' }, should_hash: { uid: 'root' }, result: true }, + { is_hash: { uid: '1' }, should_hash: { uid: 'root' }, result: false }, + ] }, + { testing: 'mac_source', property_name: :mac_source, comparisons: [ + { is_hash: { mac_source: '0A:1B:3C:4D:5E:6F' }, should_hash: { mac_source: '0a:1b:3c:4d:5e:6f' }, result: true }, + { is_hash: { mac_source: '0a:1b:3c:4d:5e:6f' }, should_hash: { mac_source: '0A:1B:3C:4D:5E:6F' }, result: true }, + { is_hash: { mac_source: '0a:1b:3c:4d:5e:6f' }, should_hash: { mac_source: '0a:1b:3c:4d:5e:6f' }, result: true }, + { is_hash: { mac_source: '! 0A:1B:3C:4D:5E:6F' }, should_hash: { mac_source: '! 0A:1B:3C:4D:5E:6F' }, result: true }, + { is_hash: { mac_source: '! 0A:1B:3C:4D:5E:6F' }, should_hash: { mac_source: '0A:1B:3C:4D:5E:6F' }, result: false }, + ] }, + { testing: 'state/ctstate/ctstatus', property_name: :state, comparisons: [ + { is_hash: { state: 'NEW' }, should_hash: { state: 'NEW' }, result: nil }, + { is_hash: { state: ['NEW'] }, should_hash: { state: 'NEW' }, result: nil }, + { is_hash: { state: 'NEW' }, should_hash: { state: ['NEW', 'INVALID'] }, result: nil }, + { is_hash: { state: ['INVALID', 'NEW'] }, should_hash: { state: ['NEW', 'INVALID'] }, result: true }, + { is_hash: { state: ['! INVALID', 'NEW'] }, should_hash: { state: ['! NEW', 'INVALID'] }, result: true }, + { is_hash: { state: ['! INVALID', 'NEW'] }, should_hash: { state: ['! NEW', 'INVALID', 'UNTRACKED'] }, result: false }, + ] }, + { testing: 'icmp', property_name: :icmp, comparisons: [ + { is_hash: { protocol: 'IPv4', icmp: 'echo-reply' }, should_hash: { protocol: 'IPv4', icmp: 0 }, result: true }, + { is_hash: { protocol: 'IPv4', icmp: '0' }, should_hash: { protocol: 'IPv4', icmp: 'echo-reply' }, result: true }, + { is_hash: { protocol: 'IPv6', icmp: 'echo-reply' }, should_hash: { protocol: 'IPv6', icmp: 129 }, result: true }, + { is_hash: { protocol: 'IPv6', icmp: '129' }, should_hash: { protocol: 'IPv6', icmp: 129 }, result: true }, + { is_hash: { protocol: 'IPv4', icmp: 'echo-reply' }, should_hash: { protocol: 'IPv4', icmp: 'echo-reply' }, result: true }, + { is_hash: { protocol: 'IPv4', icmp: '3' }, should_hash: { protocol: 'IPv4', icmp: 'echo-reply' }, result: false }, + ] }, + { testing: 'log_level', property_name: :log_level, comparisons: [ + { is_hash: { log_level: 'alert' }, should_hash: { log_level: 1 }, result: true }, + { is_hash: { log_level: 'alert' }, should_hash: { log_level: 'alert' }, result: true }, + { is_hash: { log_level: '1' }, should_hash: { log_level: 'alert' }, result: true }, + { is_hash: { log_level: '1' }, should_hash: { log_level: 1 }, result: true }, + { is_hash: { log_level: '1' }, should_hash: { log_level: 'err' }, result: false }, + ] }, + { testing: 'set_mark', property_name: :set_mark, comparisons: [ + { is_hash: { set_mark: '42/42' }, should_hash: { set_mark: '0x2a/0x2a' }, result: true }, + { is_hash: { set_mark: '0x2a/0x2a' }, should_hash: { set_mark: '42/42' }, result: true }, + { is_hash: { set_mark: '0x2a/0xffffffff' }, should_hash: { set_mark: '0x2a' }, result: true }, + { is_hash: { set_mark: '0x2a/0x2a' }, should_hash: { set_mark: '0x2a/0x2a' }, result: true }, + { is_hash: { set_mark: '0x2a/0xffffffff' }, should_hash: { set_mark: '0x2a/0x2a' }, result: false }, + ] }, + { testing: 'match_mark/connmark', property_name: :match_mark, comparisons: [ + { is_hash: { match_mark: 42 }, should_hash: { match_mark: '0x2a' }, result: true }, + { is_hash: { match_mark: '0x2a' }, should_hash: { match_mark: '42' }, result: true }, + { is_hash: { match_mark: 42 }, should_hash: { match_mark: '42' }, result: true }, + { is_hash: { match_mark: '0x2a' }, should_hash: { match_mark: '0x2a' }, result: true }, + { is_hash: { match_mark: '0x2a' }, should_hash: { match_mark: 43 }, result: false }, + ] }, + { testing: 'time_start/time_stop', property_name: :time_start, comparisons: [ + { is_hash: { time_start: '04:20:00' }, should_hash: { time_start: '4:20' }, result: true }, + { is_hash: { time_start: '04:20:00' }, should_hash: { time_start: '04:20' }, result: true }, + { is_hash: { time_start: '04:20:00' }, should_hash: { time_start: '4:20:00' }, result: true }, + { is_hash: { time_start: '04:20:00' }, should_hash: { time_start: '4:20:30' }, result: false }, + ] }, + { testing: 'jump', property_name: :jump, comparisons: [ + { is_hash: { jump: 'ACCEPT' }, should_hash: { jump: 'ACCEPT' }, result: true }, + { is_hash: { jump: 'ACCEPT' }, should_hash: { jump: 'accept' }, result: true }, + { is_hash: { jump: 'accept' }, should_hash: { jump: 'accept' }, result: true }, + { is_hash: { jump: 'accept' }, should_hash: { jump: 'drop' }, result: false }, + ] }, + { testing: 'dport/sport', property_name: :dport, comparisons: [ + { is_hash: { dport: '! 50' }, should_hash: { dport: '! 50' }, result: true }, + { is_hash: { dport: '50:60' }, should_hash: { dport: '50-60' }, result: true }, + { is_hash: { dport: ['50:60'] }, should_hash: { dport: '50-60' }, result: true }, + { is_hash: { dport: ['50:60'] }, should_hash: { dport: ['50-60'] }, result: true }, + { is_hash: { dport: ['! 50:60', '90'] }, should_hash: { dport: ['! 90', '50-60'] }, result: true }, + { is_hash: { dport: '50' }, should_hash: { dport: '90' }, result: false }, + ] }, + { testing: 'string_hex', property_name: :string_hex, comparisons: [ + { is_hash: { string_hex: '! |f4 6d 04 25 b2 02 00 0a|' }, should_hash: { string_hex: '! |f46d0425b202000a|' }, result: true }, + { is_hash: { string_hex: '|f46d0425b202000a|' }, should_hash: { string_hex: '|f4 6d 04 25 b2 02 00 0a|' }, result: true }, + { is_hash: { string_hex: '|f4 6d 04 25 b2 02 00 0a|' }, should_hash: { string_hex: '|f4 6d 04 25 b2 02 00 0a|' }, result: true }, + ] }, + # if both values are arrays + { testing: 'when comparing arrays', property_name: :week_days, comparisons: [ + { is_hash: { week_days: ['Mon', 'Tue', 'Wed'] }, should_hash: { week_days: ['Tue', 'Mon', 'Wed'] }, result: true }, + { is_hash: { week_days: ['Tue', 'Wed', 'Mon'] }, should_hash: { week_days: ['Tue', 'Mon', 'Wed'] }, result: true }, + ] }, + # if not needed; either value is nil or only one is an array + { testing: 'when defaulting to the standard comparison', property_name: :stat_packet, comparisons: [ + { is_hash: { stat_packet: 313 }, should_hash: { stat_packet: 313 }, result: nil }, + { is_hash: { stat_packet: 313 }, should_hash: { stat_packet: 42 }, result: nil }, + ] }, + ].each do |test| + context "with attributes: '#{test[:testing]}'" do + test[:comparisons].each do |comparison| + it comparison do + expect(context).to receive(:debug).with(%r{\AChecking whether '#{test[:property_name]}'}) + expect(provider.insync?(context, '001 Test Rule', test[:property_name], comparison[:is_hash], comparison[:should_hash])).to eql(comparison[:result]) + end + end + end + end + end + end +end diff --git a/spec/unit/puppet/provider/firewallchain/firewallchain_spec.rb b/spec/unit/puppet/provider/firewallchain/firewallchain_spec.rb new file mode 100644 index 000000000..91dc48bbe --- /dev/null +++ b/spec/unit/puppet/provider/firewallchain/firewallchain_spec.rb @@ -0,0 +1,350 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/resource_api' + +ensure_module_defined('Puppet::Provider::Firewallchain') +require 'puppet/provider/firewallchain/firewallchain' + +RSpec.describe Puppet::Provider::Firewallchain::Firewallchain do + describe 'Public Methods' do + subject(:provider) { described_class.new } + + let(:type) { Puppet::Type.type('firewallchain') } + let(:context) { Puppet::ResourceApi::BaseContext.new(type.type_definition.definition) } + + describe 'get(_context)' do + let(:iptables) do + ' +# Generated by iptables-save v1.8.4 on Thu Aug 10 10:15:14 2023 +*filter +:INPUT ACCEPT [62:3308] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [39:3092] +:TEST_ONE - [0:0] +COMMIT +# Completed on Thu Aug 10 10:15:14 2023 +# Generated by iptables-save v1.8.4 on Thu Aug 10 10:15:14 2023 +*raw +:PREROUTING ACCEPT [13222:23455532] +:OUTPUT ACCEPT [12523:852730] +:TEST_TWO - [0:0] +COMMIT +# Completed on Thu Aug 10 10:15:14 2023 + ' + end + let(:ip6tables) do + ' +# Generated by ip6tables-save v1.8.4 on Thu Aug 10 10:21:55 2023 +*filter +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [13:824] +COMMIT +# Completed on Thu Aug 10 10:21:55 2023 + ' + end + let(:returned_data) do + [{ name: 'INPUT:filter:IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + { name: 'FORWARD:filter:IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + { name: 'OUTPUT:filter:IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + { name: 'TEST_ONE:filter:IPv4', purge: false, ignore_foreign: false, ensure: 'present' }, + { name: 'PREROUTING:raw:IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + { name: 'OUTPUT:raw:IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + { name: 'TEST_TWO:raw:IPv4', purge: false, ignore_foreign: false, ensure: 'present' }, + { name: 'INPUT:filter:IPv6', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + { name: 'FORWARD:filter:IPv6', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + { name: 'OUTPUT:filter:IPv6', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }] + end + + it 'processes the resource' do + allow(Puppet::Util::Execution).to receive(:execute).with('iptables-save').and_return(iptables) + allow(Puppet::Util::Execution).to receive(:execute).with('ip6tables-save').and_return(ip6tables) + + expect(provider.get(context)).to eq(returned_data) + end + end + + describe 'create(context, name, should)' do + [ + { + should: { name: 'TEST_ONE:filter:IPv4', chain: 'TEST_ONE', table: 'filter', protocol: 'IPv4', purge: false, ignore_foreign: false, ensure: 'present' }, + create_command: 'iptables -t filter -N TEST_ONE' + }, + { + should: { name: 'TEST_TWO:raw:IPv6', chain: 'TEST_TWO', table: 'raw', protocol: 'IPv6', purge: false, ignore_foreign: false, ensure: 'present' }, + create_command: 'ip6tables -t raw -N TEST_TWO' + }, + ].each do |test| + it "creates the resource: '#{test[:should][:name]}'" do + expect(context).to receive(:notice).with(%r{\ACreating Chain '#{test[:should][:name]}'}) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:create_command]) + allow(PuppetX::Firewall::Utility).to receive(:persist_iptables).with(context, test[:should][:name], test[:should][:protocol]) + + provider.create(context, test[:should][:name], test[:should]) + end + end + end + + describe 'update(context, name, should, is)' do + context 'when passed valid input' do + [ + { + should: { name: 'INPUT:filter:IPv4', chain: 'INPUT', table: 'filter', protocol: 'IPv4', ensure: 'present', policy: 'drop' }, + is: { name: 'INPUT:filter:IPv4', chain: 'INPUT', table: 'filter', protocol: 'IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + update_command: 'iptables -t filter -P INPUT DROP' + }, + { + should: { name: 'OUTPUT:raw:IPv6', chain: 'OUTPUT', table: 'raw', protocol: 'IPv6', ensure: 'present', policy: 'queue' }, + is: { name: 'OUTPUT:raw:IPv6', chain: 'OUTPUT', table: 'raw', protocol: 'IPv6', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + update_command: 'ip6tables -t raw -P OUTPUT QUEUE' + }, + ].each do |test| + it "updates the resource: '#{test[:should]}'" do + expect(context).to receive(:notice).with(%r{\AUpdating Chain '#{test[:should][:name]}'}) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:update_command]) + allow(PuppetX::Firewall::Utility).to receive(:persist_iptables).with(context, test[:should][:name], test[:should][:protocol]) + + provider.update(context, test[:should][:name], test[:should], test[:is]) + end + end + end + + context 'when passed invalid input' do + [ + { + should: { name: 'INPUT:filter:IPv4', chain: 'INPUT', table: 'filter', protocol: 'IPv4', ensure: 'present', policy: 'accept' }, + is: { name: 'INPUT:filter:IPv4', chain: 'INPUT', table: 'filter', protocol: 'IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + update_command: 'iptables -t filter -P INPUT DROP' + }, + { + should: { name: 'TEST_ONE:raw:IPv6', chain: 'TEST_ONE', table: 'raw', protocol: 'IPv6', ensure: 'present', policy: 'queue' }, + is: { name: 'TEST_ONE:raw:IPv6', chain: 'TEST_ONE', table: 'raw', protocol: 'IPv6', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + update_command: 'ip6tables -t raw -P OUTPUT QUEUE' + }, + ].each do |test| + it "does not update the resource: '#{test[:should]}'" do + expect(context).not_to receive(:notice).with(%r{\AUpdating Chain '#{test[:should][:name]}'}) + expect(Puppet::Util::Execution).not_to receive(:execute).with(test[:update_command]) + + provider.update(context, test[:should][:name], test[:should], test[:is]) + end + end + end + end + + describe 'delete(context, name, is)' do + context 'with custom chains' do + [ + { + is: { name: 'TEST_ONE:filter:IPv4', chain: 'TEST_ONE', table: 'filter', protocol: 'IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + flush_command: 'iptables -t filter -F TEST_ONE', + delete_command: 'iptables -t filter -X TEST_ONE' + }, + { + is: { name: 'TEST_TWO:raw:IPv6', chain: 'TEST_TWO', table: 'raw', protocol: 'IPv6', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + flush_command: 'ip6tables -t raw -F TEST_TWO', + delete_command: 'ip6tables -t raw -X TEST_TWO' + }, + ].each do |test| + it "deletes the resource: '#{test[:is]}'" do + allow(context).to receive(:notice).with(%r{\AFlushing Chain '#{test[:is][:name]}'}) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:flush_command]) + allow(context).to receive(:notice).with(%r{\ADeleting Chain '#{test[:is][:name]}'}) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:delete_command]) + allow(PuppetX::Firewall::Utility).to receive(:persist_iptables).with(context, test[:is][:name], test[:is][:protocol]) + + provider.delete(context, test[:is][:name], test[:is]) + end + end + end + + context 'with inbuilt chains' do + [ + { + is: { name: 'INPUT:filter:IPv4', chain: 'INPUT', table: 'filter', protocol: 'IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'drop' }, + flush_command: 'iptables -t filter -F INPUT', + revert_command: 'iptables -t filter -P INPUT ACCEPT' + }, + { + is: { name: 'OUTPUT:raw:IPv6', chain: 'OUTPUT', table: 'raw', protocol: 'IPv6', purge: false, ignore_foreign: false, ensure: 'present', policy: 'queue' }, + flush_command: 'ip6tables -t raw -F OUTPUT', + revert_command: 'ip6tables -t raw -P OUTPUT ACCEPT' + }, + ].each do |test| + it "reverts the resource: '#{test[:is]}'" do + allow(context).to receive(:notice).with(%r{\AFlushing Chain '#{test[:is][:name]}'}) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:flush_command]) + allow(context).to receive(:notice).with(%r{\AReverting Internal Chain '#{test[:is][:name]}'}) + expect(Puppet::Util::Execution).to receive(:execute).with(test[:revert_command]) + allow(PuppetX::Firewall::Utility).to receive(:persist_iptables).with(context, test[:is][:name], test[:is][:protocol]) + + provider.delete(context, test[:is][:name], test[:is]) + end + end + end + end + + describe 'insync?(context, _name, property_name, _is_hash, _should_hash)' do + [ + { name: 'TEST_ONE:filter:IPv4', property_name: :name, is_hash: {}, should_hash: {}, result: nil }, + { name: 'TEST_ONE:filter:IPv4', property_name: :policy, is_hash: {}, should_hash: {}, result: nil }, + { name: 'TEST_ONE:filter:IPv4', property_name: :purge, is_hash: {}, should_hash: {}, result: true }, + { name: 'TEST_ONE:filter:IPv4', property_name: :ignore, is_hash: {}, should_hash: {}, result: true }, + { name: 'TEST_ONE:filter:IPv4', property_name: :ignore_foreign, is_hash: {}, should_hash: {}, result: true }, + { name: 'TEST_ONE:filter:IPv4', property_name: :ensure, is_hash: {}, should_hash: {}, result: nil }, + ].each do |test| + it "check value is insync: '#{test[:property_name]}'" do + expect(context).to receive(:debug).with(%r{\AChecking whether '#{test[:property_name]}'}) + expect(provider.insync?(context, test[:name], test[:property_name], test[:is_hash], test[:should_hash])).to eql(test[:result]) + end + end + end + + describe 'generate' do + let(:iptables) do + ' +# Generated by iptables-save v1.8.4 on Thu Aug 10 10:15:14 2023 +*filter +:INPUT ACCEPT [62:3308] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [39:3092] +:TEST_ONE - [0:0] +COMMIT +-A TEST_ONE -p tcp -m comment --comment "001 test rule" +-A INPUT -p tcp -m comment --comment "004 test rule" +-A TEST_ONE -p tcp -m comment --comment "ignore_this foreign" +-A TEST_ONE -p tcp -m comment --comment "foreign" +# Completed on Thu Aug 10 10:15:14 2023 +# Generated by iptables-save v1.8.4 on Thu Aug 10 10:15:14 2023 +*raw +:PREROUTING ACCEPT [13222:23455532] +:OUTPUT ACCEPT [12523:852730] +COMMIT +-A OUTPUT -p tcp -m comment --comment "003 test rule" +# Completed on Thu Aug 10 10:15:14 2023 + ' + end + let(:ip6tables) do + ' +# Generated by ip6tables-save v1.8.4 on Thu Aug 10 10:21:55 2023 +*filter +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [13:824] +:TEST_TWO - [0:0] +COMMIT +-A OUTPUT -p tcp -m comment --comment "005 test rule" +# Completed on Thu Aug 10 10:21:55 2023 +*raw +:PREROUTING ACCEPT [13222:23455532] +:OUTPUT ACCEPT [12523:852730] +COMMIT +-A TEST_TWO -p tcp -m comment --comment "002 test rule" +# Completed on Thu Aug 10 10:21:55 2023 + ' + end + + [ + { + should: { name: 'TEST_ONE:filter:IPv4', purge: true, ensure: 'present' }, + purge: ['001 test rule', '9003 ignore_this foreign', '9004 foreign'] + }, + { + should: { name: 'TEST_ONE:filter:IPv4', purge: true, ignore: 'ignore_this', ensure: 'present' }, + purge: ['001 test rule', '9004 foreign'] + }, + { + should: { name: 'TEST_ONE:filter:IPv4', purge: true, ignore_foreign: true, ensure: 'present' }, + purge: ['001 test rule'] + }, + { + should: { name: 'TEST_TWO:raw:IPv6', purge: true, ensure: 'present' }, + purge: ['002 test rule'] + }, + ].each do |test| + before(:each) do + allow(Puppet::Util::Execution).to receive(:execute).with('iptables-save').and_return(iptables) + allow(Puppet::Util::Execution).to receive(:execute).with('ip6tables-save').and_return(ip6tables) + end + + it "purge chain: '#{test[:should]}'" do + resources = provider.generate(context, test[:should][:name], {}, test[:should]) + + names = [] + resources.each do |resource| + names << resource.rsapi_current_state[:name] + end + + expect(names).to eq(test[:purge]) + end + end + end + end + + describe 'Private Methods' do + subject(:provider) { described_class } + + describe 'self.process_input(is, should)' do + [ + { + input: { + is: { title: 'INPUT:filter:IPv4', purge: false, ignore_foreign: false }, + should: { name: 'INPUT:filter:IPv4', ensure: 'present' } + }, + output: { + is: { title: 'INPUT:filter:IPv4', name: 'INPUT:filter:IPv4', chain: 'INPUT', table: 'filter', protocol: 'IPv4', purge: false, ignore_foreign: false, ensure: 'present', policy: 'accept' }, + should: { name: 'INPUT:filter:IPv4', chain: 'INPUT', table: 'filter', protocol: 'IPv4', ensure: 'present', policy: 'accept' } + } + }, + ].each do |test| + it { expect(provider.process_input(test[:input][:is], test[:input][:should])).to eql([test[:output][:is], test[:output][:should]]) } + end + end + + describe 'self.verify(_is, should)' do + [ + { + should: { name: 'PREROUTING:filter:IPv4', chain: 'PREROUTING', table: 'filter', protocol: 'IPv4', ensure: 'present', policy: 'accept' }, + error: 'INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table \'filter\'' + }, + { + should: { name: 'BROUTING:mangle:IPv4', chain: 'BROUTING', table: 'mangle', protocol: 'IPv4', ensure: 'present', policy: 'accept' }, + error: 'PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table \'mangle\'' + }, + { + should: { name: 'FORWARD:nat:IPv4', chain: 'FORWARD', table: 'nat', protocol: 'IPv4', ensure: 'present', policy: 'accept' }, + error: 'PREROUTING, POSTROUTING, INPUT, and OUTPUT are the only inbuilt chains that can be used in table \'nat\'' + }, + { + should: { name: 'PREROUTING:nat:IPv6', chain: 'PREROUTING', table: 'nat', protocol: 'IPv6', ensure: 'present', policy: 'accept' }, + error: 'table nat isn\'t valid in IPv6. You must specify \':IPv4\' as the name suffix' + }, + { + should: { name: 'INPUT:raw:IPv4', chain: 'INPUT', table: 'raw', protocol: 'IPv4', ensure: 'present', policy: 'accept' }, + error: 'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\'' + }, + { + should: { name: 'BROUTING:broute:IPv4', chain: 'BROUTING', table: 'broute', protocol: 'IPv4', ensure: 'present' }, + error: 'BROUTE is only valid with protocol \'ethernet\'' + }, + { + should: { name: 'INPUT:broute:ethernet', chain: 'INPUT', table: 'broute', protocol: 'ethernet', ensure: 'present' }, + error: 'BROUTING is the only inbuilt chain allowed on on table \'broute\'' + }, + { + should: { name: 'PREROUTING:security:IPv4', chain: 'PREROUTING', table: 'security', protocol: 'IPv4', ensure: 'present', policy: 'accept' }, + error: 'INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table \'security\'' + }, + { + should: { name: 'TEST_ONE:filter:IPv4', chain: 'TEST_ONE', table: 'filter', protocol: 'IPv4', ensure: 'present', policy: 'accept' }, + error: '\'policy\' can only be set on Internal Chains. Setting for \'TEST_ONE:filter:IPv4\' is invalid' + }, + ].each do |test| + it "Expect error: #{test[:error]}" do + expect { provider.verify({}, test[:should]) }.to raise_error(ArgumentError, test[:error]) + end + end + end + end +end diff --git a/spec/unit/puppet/provider/ip6tables_spec.rb b/spec/unit/puppet/provider/ip6tables_spec.rb deleted file mode 100644 index 0b075f14f..000000000 --- a/spec/unit/puppet/provider/ip6tables_spec.rb +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env rspec # rubocop:disable Lint/ScriptPermission : Puppet error? -# frozen_string_literal: true - -require 'spec_helper' -require 'puppet/confine/exists' - -provider_class = Puppet::Type.type(:firewall).provider(:ip6tables) -describe 'ip6tables' do # rubocop:disable RSpec/MultipleDescribes - let(:params) { { name: '000 test foo', action: 'accept' } } - let(:provider) { provider_class } - let(:resource) { Puppet::Type.type(:firewall) } - let(:ip6tables_version) { '1.4.0' } - - before :each do - end - - def stub_iptables - allow(Puppet::Type::Firewall).to receive(:defaultprovider).and_return provider - # Stub confine facts - allow(provider).to receive(:command).with(:iptables_save).and_return '/sbin/iptables-save' - - allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') - stub_const('Puppet::Type::Firewall::ProviderIp6tables::Ip6tables_version', ip6tables_version) - allow(Puppet::Util::Execution).to receive(:execute).and_return '' - allow(Puppet::Util).to receive(:which).with('iptables-save') - .and_return '/sbin/iptables-save' - end - - shared_examples 'raise error' do - it { - stub_iptables - expect { - provider.new(resource.new(params)) - }.to raise_error(Puppet::DevError, error_message) - } - end - shared_examples 'run' do - it { - stub_iptables - provider.new(resource.new(params)) - } - end - context 'when iptables 1.3' do - let(:params) { { name: '000 test foo', action: 'accept' } } - let(:error_message) { %r{The ip6tables provider is not supported on version 1\.3 of iptables} } - let(:ip6tables_version) { '1.3.10' } - - it_behaves_like 'raise error' - end - context 'when ip6tables nil' do - let(:params) { { name: '000 test foo', action: 'accept' } } - let(:error_message) { %r{The ip6tables provider is not supported on version 1\.3 of iptables} } - let(:ip6tables_version) { nil } - - it_behaves_like 'run' - end -end - -describe 'ip6tables provider' do - let(:provider6) { Puppet::Type.type(:firewall).provider(:ip6tables) } - let(:resource) do - Puppet::Type.type(:firewall).new(name: '000 test foo', - action: 'accept', - provider: 'ip6tables') - end - - before :each do - allow(Puppet::Type::Firewall).to receive(:ip6tables).and_return provider6 - allow(provider6).to receive(:command).with(:ip6tables_save).and_return '/sbin/ip6tables-save' - - # Stub iptables version - allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return '1.4.7' - - allow(Puppet::Util::Execution).to receive(:execute).and_return '' - allow(Puppet::Util).to receive(:which).with('ip6tables-save') - .and_return '/sbin/ip6tables-save' - end - - it 'is expected to be able to get a list of existing rules' do - provider6.instances.each do |rule| - expect(rule).to be_instance_of(provider6) - expect(rule.properties[:provider6].to_s).to eql provider6.name.to_s - end - end - - it 'is expected to ignore lines with fatal errors' do - allow(Puppet::Util::Execution).to receive(:execute).with(['/sbin/ip6tables-save']) - .and_return('FATAL: Could not load /lib/modules/2.6.18-028stab095.1/modules.dep: No such file or directory') - expect(provider6.instances.length).to eq 0 - end - - # Load in ruby hash for test fixtures. - load 'spec/fixtures/ip6tables/conversion_hash.rb' - - describe 'when converting rules to resources' do - ARGS_TO_HASH6.each do |test_name, data| - describe "for test data '#{test_name}'" do - let(:resource) { provider6.rule_to_hash(data[:line], data[:table], 0) } - - # If this option is enabled, make sure the parameters exactly match - if data[:compare_all] - it 'the parameter hash keys should be the same as returned by rules_to_hash' do - expect(resource.keys).to match_array(data[:params].keys) - end - end - - # Iterate across each parameter, creating an example for comparison - data[:params].each do |param_name, param_value| - it "the parameter '#{param_name}' should match #{param_value.inspect}" do - if param_value == true - expect(resource[param_name]).to be_truthy - else - expect(resource[param_name]).to eq(data[:params][param_name]) - end - end - end - end - end - end - - describe 'when working out general_args' do - HASH_TO_ARGS6.each do |test_name, data| - describe "for test data '#{test_name}'" do - let(:resource) { Puppet::Type.type(:firewall).new(data[:params]) } - let(:provider6) { Puppet::Type.type(:firewall).provider(:ip6tables) } - let(:instance) { provider6.new(resource) } - - it 'general_args should be valid' do - data[:args].unshift('--wait') if instance.general_args.flatten.include? '--wait' - expect(instance.general_args.flatten).to eql data[:args] - end - end - end - end - - describe 'when deleting ipv6 resources' do - let(:sample_rule) do - '-A INPUT -i lo -m comment --comment "001 accept all to lo interface v6" -j ACCEPT' - end - - let(:bare_sample_rule) do - '-A INPUT -i lo -m comment --comment 001 accept all to lo interface v6 -j ACCEPT' - end - - let(:resource) { provider6.rule_to_hash(sample_rule, 'filter', 0) } - let(:instance) { provider6.new(resource) } - - it 'resource[:line] looks like the original rule' do - resource[:line] == sample_rule - end - - it 'delete_args is an array' do - expect(instance.delete_args.class).to eq(Array) - end - end -end diff --git a/spec/unit/puppet/provider/iptables_chain_spec.rb b/spec/unit/puppet/provider/iptables_chain_spec.rb deleted file mode 100755 index f1a891f6e..000000000 --- a/spec/unit/puppet/provider/iptables_chain_spec.rb +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env rspec -# frozen_string_literal: true - -require 'spec_helper' -require 'puppet/confine/exists' - -describe 'iptables chain' do - describe 'iptables chain provider detection' do - let(:exists) do - Puppet::Confine::Exists - end - - before :each do - # Reset the default provider - Puppet::Type.type(:firewallchain).defaultprovider = nil - - # Stub confine facts - allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') - - # Stub lookup for /sbin/iptables & /sbin/iptables-save - allow(exists).to receive(:which).with('ebtables') - .and_return '/sbin/ebtables' - allow(exists).to receive(:which).with('ebtables-save') - .and_return '/sbin/ebtables-save' - - allow(exists).to receive(:which).with('iptables') - .and_return '/sbin/iptables' - allow(exists).to receive(:which).with('iptables-save') - .and_return '/sbin/iptables-save' - - allow(exists).to receive(:which).with('ip6tables') - .and_return '/sbin/ip6tables' - allow(exists).to receive(:which).with('ip6tables-save') - .and_return '/sbin/ip6tables-save' - - # Every other command should return false so we don't pick up any - # other providers - allow(exists).to receive(:which) { |value| - value !~ %r{(eb|ip|ip6)tables(-save)?$} - }.and_return false - end - - it 'defaults to iptables provider if /sbin/(eb|ip|ip6)tables[-save] exists' do - # Create a resource instance and make sure the provider is iptables - resource = Puppet::Type.type(:firewallchain).new(name: 'test:filter:IPv4') - expect(resource.provider.class.to_s).to eq('Puppet::Type::Firewallchain::ProviderIptables_chain') - end - end - - describe 'iptables chain provider' do - let(:provider) { Puppet::Type.type(:firewallchain).provider(:iptables_chain) } - let(:resource) do - Puppet::Type.type(:firewallchain).new(name: ':test:') - end - - before :each do - allow(Puppet::Type::Firewallchain).to receive(:defaultprovider).and_return provider - allow(provider).to receive(:command).with(:ebtables_save).and_return '/sbin/ebtables-save' - allow(provider).to receive(:command).with(:iptables_save).and_return '/sbin/iptables-save' - allow(provider).to receive(:command).with(:ip6tables_save).and_return '/sbin/ip6tables-save' - - # Pretend to return nil from iptables - allow(provider).to receive(:execute).with(['/sbin/ip6tables-save']).and_return('') - allow(provider).to receive(:execute).with(['/sbin/ebtables-save']).and_return('') - allow(provider).to receive(:execute).with(['/sbin/iptables-save']).and_return('') - end - - it 'is able to get a list of existing rules' do - provider.instances.each do |chain| - expect(chain).to be_instance_of(provider) - expect(chain.properties[:provider].to_s).to eq(provider.name.to_s) - end - end - end - - describe 'iptables chain resource parsing' do - let(:provider) { Puppet::Type.type(:firewallchain).provider(:iptables_chain) } - - before :each do - ebtables = ['BROUTE:BROUTING:ethernet', - 'BROUTE:broute:ethernet', - ':INPUT:ethernet', - ':FORWARD:ethernet', - ':OUTPUT:ethernet', - ':filter:ethernet', - ':filterdrop:ethernet', - ':filterreturn:ethernet', - 'NAT:PREROUTING:ethernet', - 'NAT:OUTPUT:ethernet', - 'NAT:POSTROUTING:ethernet'] - allow(provider).to receive(:execute).with(['/sbin/ebtables-save']).and_return(' - *broute - :BROUTING ACCEPT - :broute ACCEPT - - *filter - :INPUT ACCEPT - :FORWARD ACCEPT - :OUTPUT ACCEPT - :filter ACCEPT - :filterdrop DROP - :filterreturn RETURN - - *nat - :PREROUTING ACCEPT - :OUTPUT ACCEPT - :POSTROUTING ACCEPT - ') - - iptables = [ - 'raw:PREROUTING:IPv4', - 'raw:OUTPUT:IPv4', - 'raw:raw:IPv4', - 'mangle:PREROUTING:IPv4', - 'mangle:INPUT:IPv4', - 'mangle:FORWARD:IPv4', - 'mangle:OUTPUT:IPv4', - 'mangle:POSTROUTING:IPv4', - 'mangle:mangle:IPv4', - 'NAT:PREROUTING:IPv4', - 'NAT:OUTPUT:IPv4', - 'NAT:POSTROUTING:IPv4', - 'NAT:mangle:IPv4', - 'NAT:mangle:IPv4', - 'NAT:mangle:IPv4', - 'security:INPUT:IPv4', - 'security:FORWARD:IPv4', - 'security:OUTPUT:IPv4', - ':$5()*&%\'"^$): :IPv4', - ] - allow(provider).to receive(:execute).with(['/sbin/iptables-save']).and_return(' - # Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012 - *raw - :PREROUTING ACCEPT [12:1780] - :OUTPUT ACCEPT [19:1159] - :raw - [0:0] - COMMIT - # Completed on Mon Jan 2 01:20:06 2012 - # Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012 - *mangle - :PREROUTING ACCEPT [12:1780] - :INPUT ACCEPT [12:1780] - :FORWARD ACCEPT [0:0] - :OUTPUT ACCEPT [19:1159] - :POSTROUTING ACCEPT [19:1159] - :mangle - [0:0] - COMMIT - # Completed on Mon Jan 2 01:20:06 2012 - # Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012 - *nat - :PREROUTING ACCEPT [2242:639750] - :OUTPUT ACCEPT [5176:326206] - :POSTROUTING ACCEPT [5162:325382] - COMMIT - # Completed on Mon Jan 2 01:20:06 2012 - # Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012 - *filter - :INPUT ACCEPT [0:0] - :FORWARD DROP [0:0] - :OUTPUT ACCEPT [5673:420879] - :$5()*&%\'"^$): - [0:0] - COMMIT - # Completed on Mon Jan 2 01:20:06 2012 - ') - ip6tables = [ - 'raw:PREROUTING:IPv6', - 'raw:OUTPUT:IPv6', - 'raw:ff:IPv6', - 'mangle:PREROUTING:IPv6', - 'mangle:INPUT:IPv6', - 'mangle:FORWARD:IPv6', - 'mangle:OUTPUT:IPv6', - 'mangle:POSTROUTING:IPv6', - 'mangle:ff:IPv6', - 'security:INPUT:IPv6', - 'security:FORWARD:IPv6', - 'security:OUTPUT:IPv6', - ':INPUT:IPv6', - ':FORWARD:IPv6', - ':OUTPUT:IPv6', - ':test:IPv6', - ] - allow(provider).to receive(:execute).with(['/sbin/ip6tables-save']).and_return(' - # Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012 - *raw - :PREROUTING ACCEPT [2173:489241] - :OUTPUT ACCEPT [0:0] - :ff - [0:0] - COMMIT - # Completed on Mon Jan 2 01:31:39 2012 - # Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012 - *mangle - :PREROUTING ACCEPT [2301:518373] - :INPUT ACCEPT [0:0] - :FORWARD ACCEPT [0:0] - :OUTPUT ACCEPT [0:0] - :POSTROUTING ACCEPT [0:0] - :ff - [0:0] - COMMIT - # Completed on Mon Jan 2 01:31:39 2012 - # Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012 - *filter - :INPUT ACCEPT [0:0] - :FORWARD DROP [0:0] - :OUTPUT ACCEPT [20:1292] - :test - [0:0] - COMMIT - # Completed on Mon Jan 2 01:31:39 2012 - ') - @all = ebtables + iptables + ip6tables - # IPv4 and IPv6 names also exist as resources {table}:{chain}:IP and {table}:{chain}: - iptables.each { |name| @all += [name[0..-3], name[0..-5]] } - ip6tables.each { |name| @all += [name[0..-3], name[0..-5]] } - end - - it 'has all in parsed resources' do - provider.instances.each do |resource| - @all.include?(resource.name) # rubocop:disable RSpec/InstanceVariable - end - end - end -end diff --git a/spec/unit/puppet/provider/iptables_spec.rb b/spec/unit/puppet/provider/iptables_spec.rb deleted file mode 100644 index ea9e86dbd..000000000 --- a/spec/unit/puppet/provider/iptables_spec.rb +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env rspec # rubocop:disable Lint/ScriptPermission : Puppet error? -# frozen_string_literal: true - -require 'spec_helper' -require 'puppet/confine/exists' - -describe 'iptables provider detection' do # rubocop:disable RSpec/MultipleDescribes - let(:exists) do - Puppet::Confine::Exists - end - - before :each do - # Reset the default provider - Puppet::Type.type(:firewall).defaultprovider = nil - - # Stub confine facts - allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') - end - - it 'is expected to default to iptables provider if /sbin/iptables[-save] exists' do - # Stub lookup for /sbin/iptables & /sbin/iptables-save - allow(exists).to receive(:which).with('iptables').and_return '/sbin/iptables' - allow(exists).to receive(:which).with('iptables-save').and_return '/sbin/iptables-save' - - # Every other command is expected to return false so we don't pick up any - # other providers - allow(exists).to receive(:which) { |value| !['iptables', 'iptables-save'].include?(value) }.and_return false - - # Create a resource instance and make sure the provider is iptables - resource = Puppet::Type.type(:firewall).new(name: '000 test foo') - expect(resource.provider.class.to_s).to eq('Puppet::Type::Firewall::ProviderIptables') - end -end - -describe 'iptables provider' do - let(:provider) { Puppet::Type.type(:firewall).provider(:iptables) } - let(:resource) do - Puppet::Type.type(:firewall).new(name: '000 test foo', - action: 'accept') - end - - before :each do - allow(Puppet::Type::Firewall).to receive(:defaultprovider).and_return provider - allow(provider).to receive(:command).with(:iptables_save).and_return '/sbin/iptables-save' - - # Stub iptables version - allow(Facter.fact(:iptables_version)).to receive(:value).and_return('1.4.2') - - allow(Puppet::Util::Execution).to receive(:execute).and_return '' - allow(Puppet::Util).to receive(:which).with('iptables-save') - .and_return '/sbin/iptables-save' - end - - it 'is expected to be able to get a list of existing rules' do - provider.instances.each do |rule| - expect(rule).to be_instance_of(provider) - expect(rule.properties[:provider].to_s).to eq(provider.name.to_s) - end - end - - it 'is expected to ignore lines with fatal errors' do - allow(Puppet::Util::Execution).to receive(:execute).with(['/sbin/iptables-save']) - .and_return('FATAL: Could not load /lib/modules/2.6.18-028stab095.1/modules.dep: No such file or directory') - - expect(provider.instances.length).to be_zero - end - - describe '#insert_order' do - let(:iptables_save_output) do - [ - '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT', - '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -m comment --comment "200 test" -j ACCEPT', - '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT', - ] - end - let(:resources) do - iptables_save_output.each_with_index.map { |l, index| provider.rule_to_hash(l, 'filter', index) } - end - let(:providers) do - resources.map { |r| provider.new(r) } - end - - it 'understands offsets for adding rules to the beginning' do - resource = Puppet::Type.type(:firewall).new(name: '001 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(1) # 1-indexed - end - it 'understands offsets for editing rules at the beginning' do - resource = Puppet::Type.type(:firewall).new(name: '100 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(1) - end - it 'understands offsets for adding rules to the middle' do - resource = Puppet::Type.type(:firewall).new(name: '101 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(2) - end - it 'understands offsets for editing rules at the middle' do - resource = Puppet::Type.type(:firewall).new(name: '200 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(2) - end - it 'understands offsets for adding rules to the end' do - resource = Puppet::Type.type(:firewall).new(name: '301 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(4) - end - it 'understands offsets for editing rules at the end' do - resource = Puppet::Type.type(:firewall).new(name: '300 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(3) - end - - context 'with unname rules between' do - let(:iptables_save_output) do - [ - '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT', - '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 150 -m comment --comment "150 test" -j ACCEPT', - '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -j ACCEPT', - '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 250 -j ACCEPT', - '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT', - '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 350 -m comment --comment "350 test" -j ACCEPT', - ] - end - - it 'understands offsets for adding rules before unnamed rules' do - resource = Puppet::Type.type(:firewall).new(name: '001 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(1) - end - it 'understands offsets for editing rules before unnamed rules' do - resource = Puppet::Type.type(:firewall).new(name: '100 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(1) - end - it 'understands offsets for adding rules between managed rules' do - resource = Puppet::Type.type(:firewall).new(name: '120 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(2) - end - it 'understands offsets for adding rules between unnamed rules' do - resource = Puppet::Type.type(:firewall).new(name: '151 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(3) - end - it 'understands offsets for adding rules after unnamed rules' do - resource = Puppet::Type.type(:firewall).new(name: '351 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(7) - end - end - - context 'with unname rules before and after' do - let(:iptables_save_output) do - [ - '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 050 -j ACCEPT', - '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 090 -j ACCEPT', - '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT', - '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 150 -m comment --comment "150 test" -j ACCEPT', - '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -j ACCEPT', - '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 250 -j ACCEPT', - '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT', - '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 350 -m comment --comment "350 test" -j ACCEPT', - '-A INPUT -s 8.0.0.5/32 -p tcp -m multiport --ports 400 -j ACCEPT', - '-A INPUT -s 8.0.0.5/32 -p tcp -m multiport --ports 450 -j ACCEPT', - ] - end - - it 'understands offsets for adding rules before unnamed rules' do - resource = Puppet::Type.type(:firewall).new(name: '001 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(1) - end - it 'understands offsets for editing rules before unnamed rules' do - resource = Puppet::Type.type(:firewall).new(name: '100 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(3) - end - it 'understands offsets for adding rules between managed rules' do - resource = Puppet::Type.type(:firewall).new(name: '120 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(4) - end - it 'understands offsets for adding rules between unnamed rules' do - resource = Puppet::Type.type(:firewall).new(name: '151 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(5) - end - it 'understands offsets for adding rules after unnamed rules' do - resource = Puppet::Type.type(:firewall).new(name: '351 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(9) - end - it 'understands offsets for adding rules at the end' do - resource = Puppet::Type.type(:firewall).new(name: '950 test') - allow(resource.provider.class).to receive(:instances).and_return(providers) - expect(resource.provider.insert_order).to eq(11) - end - end - end - - # Load in ruby hash for test fixtures. - load 'spec/fixtures/iptables/conversion_hash.rb' - - describe 'when converting rules to resources' do - ARGS_TO_HASH.each do |test_name, data| - describe "for test data '#{test_name}'" do - let(:resource) { provider.rule_to_hash(data[:line], data[:table], 0) } - - # If this option is enabled, make sure the error was raised - if data[:produce_warning] - it 'the input rules should produce a warning by rules_to_hash' do - allow(provider).to receive(:warning).with(%r{Skipping unparsable iptables rule}) - resource # force resource to get evaluated - end - end - - # If this option is enabled, make sure the parameters exactly match - if data[:compare_all] - it 'the parameter hash keys should be the same as returned by rules_to_hash' do - expect(resource.keys).to match_array(data[:params].keys) - end - end - - # Iterate across each parameter, creating an example for comparison - data[:params].each do |param_name, param_value| - it "the parameter '#{param_name}' should match #{param_value.inspect}" do - # booleans get cludged to string "true" - if param_value == true - expect(resource[param_name]).to be_truthy - else - expect(resource[param_name]).to eq(data[:params][param_name]) - end - end - end - end - end - end - - describe 'when working out general_args' do - HASH_TO_ARGS.each do |test_name, data| - describe "for test data '#{test_name}'" do - let(:resource) { Puppet::Type.type(:firewall).new(data[:params]) } - let(:provider) { Puppet::Type.type(:firewall).provider(:iptables) } - let(:instance) { provider.new(resource) } - - it 'general_args should be valid' do - expect(instance.general_args.flatten).to eq(data[:args]) - end - end - end - end - - describe 'when converting rules without comments to resources' do - let(:sample_rule) do - '-A INPUT -s 1.1.1.1 -d 1.1.1.1 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -j ACCEPT' - end - let(:resource) { provider.rule_to_hash(sample_rule, 'filter', 0) } - let(:instance) { provider.new(resource) } - - it 'rule name contains a MD5 sum of the line' do - expect(resource[:name]).to eq("9000 #{Digest::SHA256.hexdigest(resource[:line])}") - end - - resource_types = [:chain, :source, :destination, :proto, :dport, :sport, :action] - rule_values = ['INPUT', '1.1.1.1/32', '1.1.1.1/32', 'tcp', ['7061', '7062'], ['7061', '7062'], 'accept'] - it 'parsed the rule arguments correctly' do - resource_types.each_with_index do |type, index| - expect(resource[type]).to eq(rule_values[index]) - end - end - end - - describe 'when converting existing rules generates by system-config-firewall-tui to resources' do - let(:sample_rule) do - # as generated by iptables-save from rules created with system-config-firewall-tui - '-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT' - end - let(:resource) { provider.rule_to_hash(sample_rule, 'filter', 0) } - let(:instance) { provider.new(resource) } - - it 'rule name contains a MD5 sum of the line' do - expect(resource[:name]).to eq("9000 #{Digest::SHA256.hexdigest(resource[:line])}") - end - - resource_types = [:chain, :proto, :dport, :state, :action] - rule_values = ['INPUT', 'tcp', ['22'], ['NEW'], 'accept'] - it 'parse arguments' do - resource_types.each_with_index do |type, index| - expect(resource[type]).to eq(rule_values[index]) - end - end - end - - describe 'when creating resources' do - let(:instance) { provider.new(resource) } - - it 'insert_args should be an array' do - expect(instance.insert_args.class).to eq(Array) - end - end - - describe 'when modifying resources' do - let(:instance) { provider.new(resource) } - - it 'update_args should be an array' do - expect(instance.update_args.class).to eq(Array) - end - - it 'fails when modifying the chain' do - expect { instance.chain = 'OUTPUT' }.to raise_error(%r{is not supported}) - end - end - - describe 'when inverting rules' do - let(:resource) do - Puppet::Type.type(:firewall).new(name: '040 partial invert', - table: 'filter', - action: 'accept', - chain: 'nova-compute-FORWARD', - source: '0.0.0.0/32', - destination: '255.255.255.255/32', - sport: ['! 78', '79', 'talk'], - dport: ['77', '! 76'], - proto: 'udp') - end - let(:instance) { provider.new(resource) } - - it 'fails when not all array items are inverted' do - expect { instance.insert }.to raise_error RuntimeError, %r{but '79', '517' are not prefixed} - end - end - - describe 'when deleting resources' do - let(:sample_rule) do - '-A INPUT -s 1.1.1.1 -d 1.1.1.1 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -j ACCEPT' - end - let(:resource) { provider.rule_to_hash(sample_rule, 'filter', 0) } - let(:instance) { provider.new(resource) } - - it 'resource[:line] looks like the original rule' do - resource[:line] == sample_rule - end - - it 'delete_args is an array' do - expect(instance.delete_args.class).to eq(Array) - end - - it 'delete_args is the same as the rule string when joined' do - expect(instance.delete_args.join(' ')).to eq(sample_rule.gsub(%r{\-A}, - '-t filter -D')) - end - end -end diff --git a/spec/unit/puppet/type/firewall_spec.rb b/spec/unit/puppet/type/firewall_spec.rb index a31f022c4..f2f280e9f 100755 --- a/spec/unit/puppet/type/firewall_spec.rb +++ b/spec/unit/puppet/type/firewall_spec.rb @@ -1,890 +1,750 @@ -#!/usr/bin/env rspec # frozen_string_literal: true require 'spec_helper' +require 'puppet/type/firewall' -firewall = Puppet::Type.type(:firewall) +RSpec.describe 'firewall type' do + let(:firewall) { Puppet::Type.type(:firewall) } -describe firewall do # rubocop:disable RSpec/MultipleDescribes - let(:firewall_class) { firewall } - let(:provider) { instance_double('provider') } - let(:resource) { firewall_class.new(name: '000 test foo') } - - before :each do - allow(provider).to receive(:name).and_return(:iptables) - allow(Puppet::Type::Firewall).to receive(:defaultprovider).and_return provider - - # Stub iptables version - allow(Facter.fact(:iptables_version)).to receive(:value).and_return('1.4.2') - allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return('1.4.2') - - # Stub confine facts - allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') + it 'loads' do + expect(firewall).not_to be_nil end it 'has :name be its namevar' do - expect(firewall_class.key_attributes).to eql [:name] - end - - describe ':name' do - it 'accepts a name' do - resource[:name] = '000-test-foo' - expect(resource[:name]).to eql '000-test-foo' - end - - it 'does not accept a name with non-ASCII chars' do - expect(-> { resource[:name] = '%*#^(#$' }).to raise_error(Puppet::Error) - end - end - - describe ':action' do - it 'has no default' do - res = firewall_class.new(name: '000 test') - expect(res.parameters[:action]).to be nil - end - - [:accept, :drop, :reject].each do |action| - it "accepts value #{action}" do - resource[:action] = action - expect(resource[:action]).to eql action - end - end - - it 'fails when value is not recognized' do - expect(-> { resource[:action] = 'not valid' }).to raise_error(Puppet::Error) - end - end - - describe ':chain' do - [:INPUT, :FORWARD, :OUTPUT, :PREROUTING, :POSTROUTING].each do |chain| - it "accepts chain value #{chain}" do - resource[:chain] = chain - expect(resource[:chain]).to eql chain - end - end - - it 'fails when the chain value is not recognized' do - expect(-> { resource[:chain] = 'not valid' }).to raise_error(Puppet::Error) - end - end - - describe ':table' do - [:nat, :mangle, :filter, :raw].each do |table| - it "accepts table value #{table}" do - resource[:table] = table - expect(resource[:table]).to eql table - end - end - - it 'fails when table value is not recognized' do - expect(-> { resource[:table] = 'not valid' }).to raise_error(Puppet::Error) - end - end - - describe ':proto' do - [:ip, :tcp, :udp, :icmp, :esp, :ah, :vrrp, :carp, :igmp, :ipencap, :ipv4, :ipv6, :ospf, :gre, :pim, :all].each do |proto| - it "accepts proto value #{proto}" do - resource[:proto] = proto - expect(resource[:proto]).to eql proto - end - end - - it 'fails when proto value is not recognized' do - expect(-> { resource[:proto] = 'foo' }).to raise_error(Puppet::Error) - end - end - - describe ':jump' do - it 'has no default' do - res = firewall_class.new(name: '000 test') - expect(res.parameters[:jump]).to be nil - end - - ['QUEUE', 'RETURN', 'DNAT', 'SNAT', 'LOG', 'NFLOG', 'MASQUERADE', 'REDIRECT', 'MARK'].each do |jump| - it "accepts jump value #{jump}" do - resource[:jump] = jump - expect(resource[:jump]).to eql jump - end - end - - ['ACCEPT', 'DROP', 'REJECT'].each do |jump| - it "nows fail when value #{jump}" do - expect(-> { resource[:jump] = jump }).to raise_error(Puppet::Error) - end - end - - it 'fails when jump value is not recognized' do - expect(-> { resource[:jump] = '%^&*' }).to raise_error(Puppet::Error) - end - end - - [:source, :destination].each do |addr| - describe addr do - it "accepts a #{addr} as a string" do - resource[addr] = '127.0.0.1' - expect(resource[addr]).to eql '127.0.0.1/32' - end - ['0.0.0.0/0', '::/0'].each do |prefix| - it "is nil for zero prefix length address #{prefix}" do - resource[addr] = prefix - expect(resource[addr]).to be nil - end - end - it "accepts a negated #{addr} as a string" do - resource[addr] = '! 127.0.0.1' - expect(resource[addr]).to eql '! 127.0.0.1/32' - end - end - end - - describe 'source error checking' do - it 'Invalid address when 256.168.2.0/24' do - expect { resource[:source] = '256.168.2.0/24' }.to raise_error( - Puppet::Error, %r{host_to_ip failed} - ) - end - end - - describe 'destination error checking' do - it 'Invalid address when 256.168.2.0/24' do - expect { resource[:destination] = '256.168.2.0/24' }.to raise_error( - Puppet::Error, %r{host_to_ip failed} - ) - end - end - - describe 'src_range error checking' do - it 'Invalid IP when 392.168.1.1-192.168.1.10' do - expect { resource[:src_range] = '392.168.1.1-192.168.1.10' }.to raise_error( - Puppet::Error, %r{Invalid IP address} - ) - end - end - - describe 'dst_range error checking' do - it 'Invalid IP when 392.168.1.1-192.168.1.10' do - expect { resource[:dst_range] = '392.168.1.1-192.168.1.10' }.to raise_error( - Puppet::Error, %r{Invalid IP address} - ) - end - end - - [:dport, :sport].each do |port| - describe port do - it "accepts a #{port} as string" do - resource[port] = '22' - expect(resource[port]).to eql ['22'] - end - - it "accepts a #{port} as an array" do - resource[port] = ['22', '23'] - expect(resource[port]).to eql ['22', '23'] - end - - it "accepts a #{port} as a number" do - resource[port] = 22 - expect(resource[port]).to eql ['22'] - end - - it "accepts a #{port} as a hyphen separated range" do - resource[port] = ['22-1000'] - expect(resource[port]).to eql ['22-1000'] - end - - it "should accept a #{port} as a combination of arrays of single and " \ - 'hyphen separated ranges' do - resource[port] = ['22-1000', '33', '3000-4000'] - expect(resource[port]).to eql ['22-1000', '33', '3000-4000'] - end - - it "converts a port name for #{port} to its number" do - resource[port] = 'ssh' - expect(resource[port]).to eql ['22'] - end - - it "does not accept something invalid for #{port}" do - expect { resource[port] = 'something odd' }.to raise_error(Puppet::Error, %r{^Parameter .+ failed.+Munging failed for value ".+" in class .+: no such service}) - end - - it "does not accept something invalid in an array for #{port}" do - expect { resource[port] = ['something odd', 'something even odder'] }.to raise_error(Puppet::Error, %r{^Parameter .+ failed.+Munging failed for value ".+" in class .+: no such service}) - end - end - end - - describe 'port deprecated' do - it 'raises a warning' do - allow(Puppet).to receive(:warning).with %r{port to firewall is deprecated} - resource[:port] = '22' - end - end - - [:dst_type, :src_type].each do |addrtype| - describe addrtype do - it 'has no default' do - res = firewall_class.new(name: '000 test') - expect(res.parameters[addrtype]).to be nil - end - end - - [:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, :BLACKHOLE, - :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].each do |type| - ['! ', ''].each do |negation| - ['', ' --limit-iface-in', ' --limit-iface-out'].each do |limit| - it "accepts #{addrtype} value #{negation}#{type}#{limit}" do - resource[addrtype] = type - expect(resource[addrtype]).to eql [type] - end - end - end - end - - it "fails when #{addrtype} value is not recognized" do - expect(-> { resource[addrtype] = 'foo' }).to raise_error(Puppet::Error) - end - end - - [:iniface, :outiface].each do |iface| - describe iface do - it "accepts #{iface} value as a string" do - resource[iface] = 'eth1' - expect(resource[iface]).to eql 'eth1' - end - it "accepts a negated #{iface} value as a string" do - resource[iface] = '! eth1' - expect(resource[iface]).to eql '! eth1' - end - it "accepts an interface alias for the #{iface} value as a string" do - resource[iface] = 'eth1:2' - expect(resource[iface]).to eql 'eth1:2' - end - end - end - - [:tosource, :todest, :to].each do |addr| - describe addr do - it "accepts #{addr} value as a string" do - resource[addr] = '127.0.0.1' - end - end - end - - describe ':log_level' do - values = { - 'panic' => '0', - 'alert' => '1', - 'crit' => '2', - 'err' => '3', - 'warn' => '4', - 'warning' => '4', - 'not' => '5', - 'notice' => '5', - 'info' => '6', - 'debug' => '7', - } - - values.each do |k, v| - it { - resource[:log_level] = k - expect(resource[:log_level]).to eql v - } - - it { - resource[:log_level] = 3 - expect(resource[:log_level]).to be 3 - } - - it { expect(-> { resource[:log_level] = 'foo' }).to raise_error(Puppet::Error) } - end - end - - describe 'NFLOG' do - describe ':nflog_group' do - [0, 1, 5, 10].each do |v| - it { - resource[:nflog_group] = v - expect(resource[:nflog_group]).to eq v - } - end - - [-3, 999_999].each do |v| - it { - expect(-> { resource[:nflog_group] = v }).to raise_error(Puppet::Error, %r{2\^16\-1}) - } - end - end - - describe ':nflog_prefix' do - let(:valid_prefix) { 'This is a valid prefix' } - let(:invalid_prefix) { 'This is not a valid prefix. !t is longer than 64 char@cters for sure. How do I know? I c0unted.' } - - it { - resource[:nflog_prefix] = valid_prefix - expect(resource[:nflog_prefix]).to eq valid_prefix - } - - it { - expect(-> { resource[:nflog_prefix] = invalid_prefix }).to raise_error(Puppet::Error, %r{64 characters}) - } - end - end - - describe ':icmp' do - icmp_codes = { - iptables: { - '0' => 'echo-reply', - '3' => 'destination-unreachable', - '4' => 'source-quench', - '6' => 'redirect', - '8' => 'echo-request', - '9' => 'router-advertisement', - '10' => 'router-solicitation', - '11' => 'time-exceeded', - '12' => 'parameter-problem', - '13' => 'timestamp-request', - '14' => 'timestamp-reply', - '17' => 'address-mask-request', - '18' => 'address-mask-reply', - }, - ip6tables: { - '1' => 'destination-unreachable', - '2' => 'too-big', - '3' => 'time-exceeded', - '4' => 'parameter-problem', - '128' => 'echo-request', - '129' => 'echo-reply', - '133' => 'router-solicitation', - '134' => 'router-advertisement', - '137' => 'redirect', - }, + expect(firewall.key_attributes).to eql [:name] + end + + describe ':line' do + it 'is read only' do + expect { firewall.new(name: '001 test rule', line: 'test') }.to raise_error(Puppet::Error) + end + end + + { + ':ensure': { + valid: [{ name: '001 test rule', ensure: 'present' }, { name: '001 test rule', ensure: 'absent' }], + invalid: [{ name: '001 test rule', ensure: true }, { name: '001 test rule', ensure: 313 }, { name: '001 test rule', ensure: 'false' }] + }, + ':name': { + valid: [{ name: '001 first' }, { name: '202 second rule' }, { name: '333 third rule also' }], + invalid: [{ name: 'invalid rule 001' }, { name: 'invalid rule two' }] + }, + ':protocol': { + valid: [{ name: '001 test rule', protocol: 'iptables' }, { name: '001 test rule', protocol: 'ip6tables' }, + { name: '001 test rule', protocol: 'IPv4' }, { name: '001 test rule', protocol: 'IPv6' }], + invalid: [{ name: '001 test rule', protocol: true }, { name: '001 test rule', protocol: 313 }, + { name: '001 test rule', protocol: 'IPv6tables' }] + }, + ':table': { + valid: [{ name: '001 test rule', table: 'nat' }, { name: '001 test rule', table: 'raw' }, + { name: '001 test rule', table: 'broute' }, { name: '001 test rule', table: 'security' }], + invalid: [{ name: '001 test rule', table: true }, { name: '001 test rule', table: 313 }, + { name: '001 test rule', table: 'rawroute' }] + }, + ':chain': { + valid: [{ name: '001 test rule', chain: 'INPUT' }, { name: '001 test rule', chain: 'OUTPUT' }, + { name: '001 test rule', chain: 'POSTROUTING' }, { name: '001 test rule', chain: 'test_chain' }], + invalid: [{ name: '001 test rule', chain: true }, { name: '001 test rule', chain: 313 }, + { name: '001 test rule', chain: '' }] + }, + ':source': { + valid: [{ name: '001 test rule', source: '192.168.2.0/24' }, { name: '001 test rule', source: '! 192.168.2.0/24' }, + { name: '001 test rule', source: '1::46' }, { name: '001 test rule', source: '! 1::46' }], + invalid: [{ name: '001 test rule', source: true }, { name: '001 test rule', source: 313 }, + { name: '001 test rule', source: '' }] + }, + ':destination': { + valid: [{ name: '001 test rule', destination: '192.168.2.0/24' }, { name: '001 test rule', destination: '! 192.168.2.0/24' }, + { name: '001 test rule', destination: '1::46' }, { name: '001 test rule', destination: '! 1::46' }], + invalid: [{ name: '001 test rule', destination: true }, { name: '001 test rule', destination: 313 }, + { name: '001 test rule', destination: '' }] + }, + ':iniface': { + valid: [{ name: '001 test rule', iniface: 'enp0s8' }, { name: '001 test rule', iniface: '! enp0s8' }, + { name: '001 test rule', iniface: 'lo' }, { name: '001 test rule', iniface: '! lo' }], + invalid: [{ name: '001 test rule', iniface: true }, { name: '001 test rule', iniface: 313 }, + { name: '001 test rule', iniface: 'invalid/' }, { name: '001 test rule', iniface: '' }] + }, + ':outiface': { + valid: [{ name: '001 test rule', outiface: 'enp0s8' }, { name: '001 test rule', outiface: '! enp0s8' }, + { name: '001 test rule', outiface: 'lo' }, { name: '001 test rule', outiface: '! lo' }], + invalid: [{ name: '001 test rule', outiface: true }, { name: '001 test rule', outiface: 313 }, + { name: '001 test rule', outiface: 'invalid/' }, { name: '001 test rule', outiface: '' }] + }, + ':physdev_in': { + valid: [{ name: '001 test rule', physdev_in: 'enp0s8' }, { name: '001 test rule', physdev_in: '! enp0s8' }, + { name: '001 test rule', physdev_in: 'lo' }, { name: '001 test rule', physdev_in: '! lo' }], + invalid: [{ name: '001 test rule', physdev_in: true }, { name: '001 test rule', physdev_in: 313 }, + { name: '001 test rule', physdev_in: 'invalid/' }, { name: '001 test rule', physdev_in: '' }] + }, + ':physdev_out': { + valid: [{ name: '001 test rule', physdev_out: 'enp0s8' }, { name: '001 test rule', physdev_out: '! enp0s8' }, + { name: '001 test rule', physdev_out: 'lo' }, { name: '001 test rule', physdev_out: '! lo' }], + invalid: [{ name: '001 test rule', physdev_out: true }, { name: '001 test rule', physdev_out: 313 }, + { name: '001 test rule', physdev_out: 'invalid/' }, { name: '001 test rule', physdev_out: '' }] + }, + ':physdev_is_bridged': { + valid: [{ name: '001 test rule', physdev_is_bridged: true }, { name: '001 test rule', physdev_is_bridged: false }], + invalid: [{ name: '001 test rule', physdev_is_bridged: 'invalid' }, { name: '001 test rule', physdev_is_bridged: 313 }] + }, + ':physdev_is_in': { + valid: [{ name: '001 test rule', physdev_is_in: true }, { name: '001 test rule', physdev_is_in: false }], + invalid: [{ name: '001 test rule', physdev_is_in: 'invalid' }, { name: '001 test rule', physdev_is_in: 313 }] + }, + ':physdev_is_out': { + valid: [{ name: '001 test rule', physdev_is_out: true }, { name: '001 test rule', physdev_is_out: false }], + invalid: [{ name: '001 test rule', physdev_is_out: 'invalid' }, { name: '001 test rule', physdev_is_out: 313 }] + }, + ':proto': { + valid: [{ name: '001 test rule', proto: 'ipencap' }, { name: '001 test rule', proto: 'gre' }, + { name: '001 test rule', proto: 'ip' }, { name: '001 test rule', proto: 'udp' }], + invalid: [{ name: '001 test rule', proto: 'invalid' }, { name: '001 test rule', proto: 313 }] + }, + ':isfragment': { + valid: [{ name: '001 test rule', isfragment: true }, { name: '001 test rule', isfragment: false }], + invalid: [{ name: '001 test rule', isfragment: 'invalid' }, { name: '001 test rule', isfragment: 313 }] + }, + ':isfirstfrag': { + valid: [{ name: '001 test rule', isfirstfrag: true }, { name: '001 test rule', isfirstfrag: false }], + invalid: [{ name: '001 test rule', isfirstfrag: 'invalid' }, { name: '001 test rule', isfirstfrag: 313 }] + }, + ':ishasmorefrags': { + valid: [{ name: '001 test rule', ishasmorefrags: true }, { name: '001 test rule', ishasmorefrags: false }], + invalid: [{ name: '001 test rule', ishasmorefrags: 'invalid' }, { name: '001 test rule', ishasmorefrags: 313 }] + }, + ':islastfrag': { + valid: [{ name: '001 test rule', islastfrag: true }, { name: '001 test rule', islastfrag: false }], + invalid: [{ name: '001 test rule', islastfrag: 'invalid' }, { name: '001 test rule', islastfrag: 313 }] + }, + ':stat_mode': { + valid: [{ name: '001 test rule', stat_mode: 'nth' }, { name: '001 test rule', stat_mode: 'random' }], + invalid: [{ name: '001 test rule', stat_mode: 'invalid' }, { name: '001 test rule', stat_mode: 313 }] + }, + ':stat_every': { + valid: [{ name: '001 test rule', stat_every: 1 }, { name: '001 test rule', stat_every: 313 }], + invalid: [{ name: '001 test rule', stat_every: 'invalid' }, { name: '001 test rule', stat_every: false }] + }, + ':stat_packet': { + valid: [{ name: '001 test rule', stat_packet: 1 }, { name: '001 test rule', stat_packet: 313 }], + invalid: [{ name: '001 test rule', stat_packet: 'invalid' }, { name: '001 test rule', stat_packet: false }] + }, + ':stat_probability': { + valid: [{ name: '001 test rule', stat_probability: 1 }, { name: '001 test rule', stat_probability: 0 }, + { name: '001 test rule', stat_probability: 0.15 }, { name: '001 test rule', stat_probability: 0.313 }], + invalid: [{ name: '001 test rule', stat_probability: 'invalid' }, { name: '001 test rule', stat_probability: false }, + { name: '001 test rule', stat_probability: 15 }, { name: '001 test rule', stat_probability: 313 }] + }, + ':src_range': { + valid: [{ name: '001 test rule', src_range: '192.168.1.1-192.168.1.10' }, { name: '001 test rule', src_range: '! 192.168.1.1-192.168.1.10' }, + { name: '001 test rule', src_range: '1::46-2::48' }, { name: '001 test rule', src_range: '! 1::46-2::48' }], + invalid: [{ name: '001 test rule', src_range: true }, { name: '001 test rule', src_range: 313 }, + { name: '001 test rule', src_range: '' }] + }, + ':dst_range': { + valid: [{ name: '001 test rule', dst_range: '192.168.1.1-192.168.1.10' }, { name: '001 test rule', dst_range: '! 192.168.1.1-192.168.1.10' }, + { name: '001 test rule', dst_range: '1::46-2::48' }, { name: '001 test rule', dst_range: '! 1::46-2::48' }], + invalid: [{ name: '001 test rule', dst_range: true }, { name: '001 test rule', dst_range: 313 }, + { name: '001 test rule', dst_range: '' }] + }, + ':tcp_option': { + valid: [{ name: '001 test rule', tcp_option: '! 1' }, { name: '001 test rule', tcp_option: '15' }, + { name: '001 test rule', tcp_option: 121 }, { name: '001 test rule', tcp_option: '! 255' }], + invalid: [{ name: '001 test rule', tcp_option: 'invalid' }, { name: '001 test rule', tcp_option: false }, + { name: '001 test rule', tcp_option: 313 }, { name: '001 test rule', tcp_option: '313' }] + }, + ':tcp_flags': { + valid: [{ name: '001 test rule', tcp_flags: 'FIN FIN,SYN,RST,ACK' }, { name: '001 test rule', tcp_flags: '! FIN,SYN,RST,ACK SYN' }], + invalid: [{ name: '001 test rule', tcp_flags: 'invalid' }, { name: '001 test rule', tcp_flags: false }, + { name: '001 test rule', tcp_flags: 313 }, { name: '001 test rule', tcp_flags: 'FIN,SYN,RST,ACK' }] + }, + ':uid': { + valid: [{ name: '001 test rule', uid: 'testuser' }, { name: '001 test rule', uid: '! 0' }, + { name: '001 test rule', uid: 4 }, { name: '001 test rule', uid: 0 }], + invalid: [{ name: '001 test rule', uid: 0.3 }, { name: '001 test rule', uid: false }, + { name: '001 test rule', uid: '' }] + }, + ':gid': { + valid: [{ name: '001 test rule', gid: 'testuser' }, { name: '001 test rule', gid: '! 0' }, + { name: '001 test rule', gid: 4 }, { name: '001 test rule', gid: 0 }], + invalid: [{ name: '001 test rule', gid: 0.3 }, { name: '001 test rule', gid: false }, + { name: '001 test rule', gid: '' }] + }, + ':mac_source': { + valid: [{ name: '001 test rule', mac_source: 'FA:16:00:00:00:00' }, { name: '001 test rule', mac_source: '! FA:16:00:00:00:00' }], + invalid: [{ name: '001 test rule', mac_source: 'FA1600000000' }, { name: '001 test rule', mac_source: false }, + { name: '001 test rule', mac_source: 1_600_000_000 }] + }, + ':sport': { + valid: [{ name: '001 test rule', sport: '1:1024' }, { name: '001 test rule', sport: '! 1-1024' }, + { name: '001 test rule', sport: 1024 }, { name: '001 test rule', sport: '! 1024' }, + { name: '001 test rule', sport: ['1:1024', '2024'] }, { name: '001 test rule', sport: ['! 1', 1024] }], + invalid: [{ name: '001 test rule', sport: 'invalid' }, { name: '001 test rule', sport: false }] + }, + ':dport': { + valid: [{ name: '001 test rule', dport: '1:1024' }, { name: '001 test rule', dport: '! 1-1024' }, + { name: '001 test rule', dport: 1024 }, { name: '001 test rule', dport: '! 1024' }, + { name: '001 test rule', dport: ['1:1024', '1024'] }, { name: '001 test rule', dport: ['! 1', 1024] }], + invalid: [{ name: '001 test rule', dport: 'invalid' }, { name: '001 test rule', dport: false }] + }, + ':src_type': { + valid: [{ name: '001 test rule', src_type: 'LOCAL' }, { name: '001 test rule', src_type: '! BROADCAST' }, + { name: '001 test rule', src_type: 'ANYCAST --limit-iface-in' }, + { name: '001 test rule', src_type: ['! LOCAL', 'PROHIBIT --limit-iface-in'] }], + invalid: [{ name: '001 test rule', src_type: 'local' }, { name: '001 test rule', src_type: false }, + { name: '001 test rule', src_type: 123 }, { name: '001 test rule', src_type: ['unicast', 123] }] + }, + ':dst_type': { + valid: [{ name: '001 test rule', dst_type: 'LOCAL' }, { name: '001 test rule', dst_type: '! BROADCAST' }, + { name: '001 test rule', dst_type: 'ANYCAST --limit-iface-in' }, + { name: '001 test rule', dst_type: ['! LOCAL', 'PROHIBIT --limit-iface-in'] }], + invalid: [{ name: '001 test rule', dst_type: 'local' }, { name: '001 test rule', dst_type: false }, + { name: '001 test rule', dst_type: 123 }, { name: '001 test rule', dst_type: ['unicast', 123] }] + }, + ':socket': { + valid: [{ name: '001 test rule', socket: true }, { name: '001 test rule', socket: false }], + invalid: [{ name: '001 test rule', socket: 'invalid' }, { name: '001 test rule', socket: 313 }] + }, + ':pkttype': { + valid: [{ name: '001 test rule', pkttype: 'unicast' }, { name: '001 test rule', uid: 'broadcast' }, + { name: '001 test rule', pkttype: 'multicast' }], + invalid: [{ name: '001 test rule', pkttype: 'invalid' }, { name: '001 test rule', pkttype: false }, + { name: '001 test rule', pkttype: 313 }] + }, + ':ipsec_dir': { + valid: [{ name: '001 test rule', ipsec_dir: 'in' }, { name: '001 test rule', ipsec_dir: 'out' }], + invalid: [{ name: '001 test rule', ipsec_dir: 'invalid' }, { name: '001 test rule', ipsec_dir: false }, + { name: '001 test rule', ipsec_dir: 313 }] + }, + ':ipsec_policy': { + valid: [{ name: '001 test rule', ipsec_policy: 'none' }, { name: '001 test rule', ipsec_policy: 'ipsec' }], + invalid: [{ name: '001 test rule', ipsec_policy: 'invalid' }, { name: '001 test rule', ipsec_policy: false }, + { name: '001 test rule', ipsec_policy: 313 }] + }, + ':state': { + valid: [{ name: '001 test rule', state: 'INVALID' }, { name: '001 test rule', state: '! ESTABLISHED' }, + { name: '001 test rule', state: ['! UNTRACKED', 'INVALID'] }], + invalid: [{ name: '001 test rule', state: 'invalid' }, { name: '001 test rule', state: false }] + }, + ':ctstate': { + valid: [{ name: '001 test rule', ctstate: 'INVALID' }, { name: '001 test rule', ctstate: '! ESTABLISHED' }, + { name: '001 test rule', ctstate: ['! UNTRACKED', 'INVALID'] }], + invalid: [{ name: '001 test rule', ctstate: 'invalid' }, { name: '001 test rule', ctstate: false }] + }, + ':ctproto': { + valid: [{ name: '001 test rule', ctproto: '! 1' }, { name: '001 test rule', ctproto: '15' }, + { name: '001 test rule', ctproto: 313 }, { name: '001 test rule', ctproto: '! 313' }], + invalid: [{ name: '001 test rule', ctproto: 'invalid' }, { name: '001 test rule', ctproto: false }] + }, + ':ctorigsrc': { + valid: [{ name: '001 test rule', ctorigsrc: '192.168.2.0/24' }, { name: '001 test rule', ctorigsrc: '! 192.168.2.0/24' }, + { name: '001 test rule', ctorigsrc: '1::46' }, { name: '001 test rule', ctorigsrc: '! 1::46' }], + invalid: [{ name: '001 test rule', ctorigsrc: true }, { name: '001 test rule', ctorigsrc: 313 }, + { name: '001 test rule', ctorigsrc: '' }] + }, + ':ctorigdst': { + valid: [{ name: '001 test rule', ctorigdst: '192.168.2.0/24' }, { name: '001 test rule', ctorigdst: '! 192.168.2.0/24' }, + { name: '001 test rule', ctorigdst: '1::46' }, { name: '001 test rule', ctorigdst: '! 1::46' }], + invalid: [{ name: '001 test rule', ctorigdst: true }, { name: '001 test rule', ctorigdst: 313 }, + { name: '001 test rule', ctorigdst: '' }] + }, + ':ctreplsrc': { + valid: [{ name: '001 test rule', ctreplsrc: '192.168.2.0/24' }, { name: '001 test rule', ctreplsrc: '! 192.168.2.0/24' }, + { name: '001 test rule', ctreplsrc: '1::46' }, { name: '001 test rule', ctreplsrc: '! 1::46' }], + invalid: [{ name: '001 test rule', ctreplsrc: true }, { name: '001 test rule', ctreplsrc: 313 }, + { name: '001 test rule', ctreplsrc: '' }] + }, + ':ctrepldst': { + valid: [{ name: '001 test rule', ctrepldst: '192.168.2.0/24' }, { name: '001 test rule', ctrepldst: '! 192.168.2.0/24' }, + { name: '001 test rule', ctrepldst: '1::46' }, { name: '001 test rule', ctrepldst: '! 1::46' }], + invalid: [{ name: '001 test rule', ctrepldst: true }, { name: '001 test rule', ctrepldst: 313 }, + { name: '001 test rule', ctrepldst: '' }] + }, + ':ctorigsrcport': { + valid: [{ name: '001 test rule', ctorigsrcport: '80' }, { name: '001 test rule', ctorigsrcport: '! 80' }, + { name: '001 test rule', ctorigsrcport: '80:90' }, { name: '001 test rule', ctorigsrcport: '! 80:90' }], + invalid: [{ name: '001 test rule', ctorigsrcport: true }, { name: '001 test rule', ctorigsrcport: 313 }, + { name: '001 test rule', ctorigsrcport: 'invalid' }] + }, + ':ctorigdstport': { + valid: [{ name: '001 test rule', ctorigdstport: '80' }, { name: '001 test rule', ctorigdstport: '! 80' }, + { name: '001 test rule', ctorigdstport: '80:90' }, { name: '001 test rule', ctorigdstport: '! 80:90' }], + invalid: [{ name: '001 test rule', ctorigdstport: true }, { name: '001 test rule', ctorigdstport: 313 }, + { name: '001 test rule', ctorigdstport: 'invalid' }] + }, + ':ctreplsrcport': { + valid: [{ name: '001 test rule', ctreplsrcport: '80' }, { name: '001 test rule', ctreplsrcport: '! 80' }, + { name: '001 test rule', ctreplsrcport: '80:90' }, { name: '001 test rule', ctreplsrcport: '! 80:90' }], + invalid: [{ name: '001 test rule', ctreplsrcport: true }, { name: '001 test rule', ctreplsrcport: 313 }, + { name: '001 test rule', ctreplsrcport: 'invalid' }] + }, + ':ctrepldstport': { + valid: [{ name: '001 test rule', ctrepldstport: '80' }, { name: '001 test rule', ctrepldstport: '! 80' }, + { name: '001 test rule', ctrepldstport: '80:90' }, { name: '001 test rule', ctrepldstport: '! 80:90' }], + invalid: [{ name: '001 test rule', ctrepldstport: true }, { name: '001 test rule', ctrepldstport: 313 }, + { name: '001 test rule', ctrepldstport: 'invalid' }] + }, + ':ctstatus': { + valid: [{ name: '001 test rule', ctstatus: 'EXPECTED' }, { name: '001 test rule', ctstatus: '! CONFIRMED' }, + { name: '001 test rule', ctstatus: ['! EXPECTED', 'CONFIRMED'] }], + invalid: [{ name: '001 test rule', ctstatus: 'invalid' }, { name: '001 test rule', ctstatus: false }] + }, + ':ctexpire': { + valid: [{ name: '001 test rule', ctexpire: '80' }, { name: '001 test rule', ctexpire: '80:160' }], + invalid: [{ name: '001 test rule', ctexpire: true }, { name: '001 test rule', ctexpire: 313 }, + { name: '001 test rule', ctexpire: 'invalid' }] + }, + ':ctdir': { + valid: [{ name: '001 test rule', ctdir: 'REPLY' }, { name: '001 test rule', ctdir: 'ORIGINAL' }], + invalid: [{ name: '001 test rule', ctstate: 'invalid' }, { name: '001 test rule', ctstate: false }] + }, + ':hoplimit': { + valid: [{ name: '001 test rule', hop_limit: '! 1' }, { name: '001 test rule', hop_limit: '15' }, + { name: '001 test rule', hop_limit: 313 }, { name: '001 test rule', hop_limit: '! 313' }], + invalid: [{ name: '001 test rule', hop_limit: 'invalid' }, { name: '001 test rule', hop_limit: false }] + }, + ':icmp': { + valid: [{ name: '001 test rule', icmp: 'echo-reply' }, { name: '001 test rule', icmp: '15' }, + { name: '001 test rule', icmp: 313 }], + invalid: [{ name: '001 test rule', icmp: true }, { name: '001 test rule', icmp: '' }] + }, + ':limit': { + valid: [{ name: '001 test rule', limit: '50/sec' }, { name: '001 test rule', limit: '50/second' }, + { name: '001 test rule', limit: '40/min' }, { name: '001 test rule', limit: '40/minute' }, + { name: '001 test rule', limit: '30/hour' }, { name: '001 test rule', limit: '10/day' }], + invalid: [{ name: '001 test rule', limit: true }, { name: '001 test rule', limit: 30 }, + { name: '001 test rule', limit: 'invalid' }] + }, + ':burst': { + valid: [{ name: '001 test rule', burst: 1 }, { name: '001 test rule', burst: 313 }], + invalid: [{ name: '001 test rule', burst: 'invalid' }, { name: '001 test rule', burst: false }] + }, + ':length': { + valid: [{ name: '001 test rule', length: '80' }, { name: '001 test rule', length: '80:90' }], + invalid: [{ name: '001 test rule', length: true }, { name: '001 test rule', length: 313 }, + { name: '001 test rule', length: 'invalid' }] + }, + ':recent': { + valid: [{ name: '001 test rule', recent: 'set' }, { name: '001 test rule', recent: 'update' }, + { name: '001 test rule', recent: 'rcheck' }, { name: '001 test rule', recent: 'remove' }, + { name: '001 test rule', recent: '! set' }, { name: '001 test rule', recent: '! update' }, + { name: '001 test rule', recent: '! rcheck' }, { name: '001 test rule', recent: '! remove' }], + invalid: [{ name: '001 test rule', recent: true }, { name: '001 test rule', recent: 30 }, + { name: '001 test rule', recent: 'invalid' }] + }, + ':rseconds': { + valid: [{ name: '001 test rule', rseconds: 1 }, { name: '001 test rule', rseconds: 313 }], + invalid: [{ name: '001 test rule', rseconds: 'invalid' }, { name: '001 test rule', rseconds: false }, + { name: '001 test rule', rseconds: 0 }] + }, + ':reap': { + valid: [{ name: '001 test rule', reap: true }, { name: '001 test rule', reap: false }], + invalid: [{ name: '001 test rule', reap: 'invalid' }, { name: '001 test rule', reap: 313 }] + }, + ':rhitcount': { + valid: [{ name: '001 test rule', rhitcount: 1 }, { name: '001 test rule', rhitcount: 313 }], + invalid: [{ name: '001 test rule', rhitcount: 'invalid' }, { name: '001 test rule', rhitcount: false }, + { name: '001 test rule', rhitcount: 0 }] + }, + ':rttl': { + valid: [{ name: '001 test rule', rttl: true }, { name: '001 test rule', rttl: false }], + invalid: [{ name: '001 test rule', rttl: 'invalid' }, { name: '001 test rule', rttl: 313 }] + }, + ':rname': { + valid: [{ name: '001 test rule', rname: 'list1' }, { name: '001 test rule', rname: 'list2' }], + invalid: [{ name: '001 test rule', rname: true }, { name: '001 test rule', rname: 30 }, + { name: '001 test rule', rname: '' }] + }, + ':mask': { + valid: [{ name: '001 test rule', mask: '255.255.255.255' }, { name: '001 test rule', mask: '1.1.1.1' }], + invalid: [{ name: '001 test rule', mask: true }, { name: '001 test rule', mask: 30 }, + { name: '001 test rule', mask: 'invalid' }] + }, + ':rsource': { + valid: [{ name: '001 test rule', rsource: true }, { name: '001 test rule', rsource: false }], + invalid: [{ name: '001 test rule', rsource: 'invalid' }, { name: '001 test rule', rsource: 313 }] + }, + ':rdest': { + valid: [{ name: '001 test rule', rdest: true }, { name: '001 test rule', rdest: false }], + invalid: [{ name: '001 test rule', rdest: 'invalid' }, { name: '001 test rule', rdest: 313 }] + }, + ':ipset': { + valid: [{ name: '001 test rule', ipset: 'setname1 src' }, { name: '001 test rule', ipset: '! setname2 dst' }, + { name: '001 test rule', ipset: ['setname1 src', '! setname2 dst'] }], + invalid: [{ name: '001 test rule', ipset: 'invalid' }, { name: '001 test rule', ipset: false }, + { name: '001 test rule', ipset: false }] + }, + ':string': { + valid: [{ name: '001 test rule', string: 'GET /index.html' }], + invalid: [{ name: '001 test rule', string: '' }, { name: '001 test rule', string: false }, + { name: '001 test rule', string: false }] + }, + ':string_hex': { + valid: [{ name: '001 test rule', string_hex: '|f4 6d 04 25 b2 02 00 0a|' }, { name: '001 test rule', string_hex: '! |0000ff0001|' }], + invalid: [{ name: '001 test rule', string_hex: 'invalid' }, { name: '001 test rule', string_hex: false }, + { name: '001 test rule', string_hex: false }] + }, + ':string_algo': { + valid: [{ name: '001 test rule', string_algo: 'bm' }, { name: '001 test rule', string_algo: 'kmp' }], + invalid: [{ name: '001 test rule', string_algo: 'invalid' }, { name: '001 test rule', string_algo: false }, + { name: '001 test rule', string_algo: false }] + }, + ':string_from': { + valid: [{ name: '001 test rule', string_from: 1 }, { name: '001 test rule', string_from: 313 }], + invalid: [{ name: '001 test rule', string_from: 'invalid' }, { name: '001 test rule', string_from: false }, + { name: '001 test rule', string_from: 0 }] + }, + ':string_to': { + valid: [{ name: '001 test rule', string_to: 1 }, { name: '001 test rule', string_to: 313 }], + invalid: [{ name: '001 test rule', string_to: 'invalid' }, { name: '001 test rule', string_to: false }, + { name: '001 test rule', string_to: 0 }] + }, + ':jump': { + valid: [{ name: '001 test rule', jump: 'QUEUE' }, { name: '001 test rule', jump: 'test_chain' }], + invalid: [{ name: '001 test rule', jump: '' }, { name: '001 test rule', jump: false }, + { name: '001 test rule', jump: false }] + }, + ':goto': { + valid: [{ name: '001 test rule', goto: 'QUEUE' }, { name: '001 test rule', goto: 'test_chain' }], + invalid: [{ name: '001 test rule', goto: '' }, { name: '001 test rule', goto: false }, + { name: '001 test rule', goto: false }] + }, + ':clusterip_new': { + valid: [{ name: '001 test rule', clusterip_new: true }, { name: '001 test rule', clusterip_new: false }], + invalid: [{ name: '001 test rule', clusterip_new: 'invalid' }, { name: '001 test rule', clusterip_new: 313 }] + }, + ':clusterip_hashmode': { + valid: [{ name: '001 test rule', clusterip_hashmode: 'sourceip' }, { name: '001 test rule', clusterip_hashmode: 'sourceip-sourceport' }, + { name: '001 test rule', clusterip_hashmode: 'sourceip-sourceport-destport' }], + invalid: [{ name: '001 test rule', clusterip_hashmode: 'invalid' }, { name: '001 test rule', clusterip_hashmode: false }, + { name: '001 test rule', clusterip_hashmode: false }] + }, + ':clusterip_clustermac': { + valid: [{ name: '001 test rule', clusterip_clustermac: 'FA:16:00:00:00:00' }], + invalid: [{ name: '001 test rule', clusterip_clustermac: 'FA1600000000' }, { name: '001 test rule', clusterip_clustermac: false }, + { name: '001 test rule', clusterip_clustermac: 1_600_000_000 }] + }, + ':clusterip_total_nodes': { + valid: [{ name: '001 test rule', clusterip_total_nodes: 1 }, { name: '001 test rule', clusterip_total_nodes: 313 }], + invalid: [{ name: '001 test rule', clusterip_total_nodes: 'invalid' }, { name: '001 test rule', clusterip_total_nodes: false }] + }, + ':clusterip_local_node': { + valid: [{ name: '001 test rule', clusterip_local_node: 1 }, { name: '001 test rule', clusterip_local_node: 313 }], + invalid: [{ name: '001 test rule', clusterip_local_node: 'invalid' }, { name: '001 test rule', clusterip_local_node: false }] + }, + ':clusterip_hash_init': { + valid: [{ name: '001 test rule', clusterip_hash_init: 'random' }], + invalid: [{ name: '001 test rule', clusterip_hash_init: '' }, { name: '001 test rule', clusterip_hash_init: false }, + { name: '001 test rule', clusterip_hash_init: 313 }] + }, + ':queue_num': { + valid: [{ name: '001 test rule', queue_num: 1 }, { name: '001 test rule', queue_num: 313 }], + invalid: [{ name: '001 test rule', queue_num: 'invalid' }, { name: '001 test rule', queue_num: false }, + { name: '001 test rule', queue_num: 0 }] + }, + ':queue_bypass': { + valid: [{ name: '001 test rule', queue_bypass: true }, { name: '001 test rule', queue_bypass: false }], + invalid: [{ name: '001 test rule', queue_bypass: 'invalid' }, { name: '001 test rule', queue_bypass: 313 }] + }, + ':nflog_group': { + valid: [{ name: '001 test rule', nflog_group: 1 }, { name: '001 test rule', queue_num: 313 }], + invalid: [{ name: '001 test rule', nflog_group: 'invalid' }, { name: '001 test nflog_group', nflog_group: false }, + { name: '001 test rule', nflog_group: 0 }, { name: '001 test rule', nflog_group: 65_536 }] + }, + ':nflog_prefix': { + valid: [{ name: '001 test rule', nflog_prefix: 'Prefix Number One' }], + invalid: [{ name: '001 test rule', nflog_prefix: 313 }, { name: '001 test rule', nflog_prefix: false }] + # { name: '001 test rule', nflog_prefix: '' } attribute throws errors when type set to String[1] + }, + ':nflog_range': { + valid: [{ name: '001 test rule', nflog_range: 1 }, { name: '001 test rule', nflog_range: 313 }], + invalid: [{ name: '001 test rule', nflog_range: 'invalid' }, { name: '001 test rule', nflog_range: false }, + { name: '001 test rule', nflog_range: 0 }] + }, + ':nflog_size': { + valid: [{ name: '001 test rule', nflog_size: 1 }, { name: '001 test rule', nflog_size: 313 }], + invalid: [{ name: '001 test rule', nflog_size: 'invalid' }, { name: '001 test rule', nflog_size: false }, + { name: '001 test rule', nflog_size: 0 }] + }, + ':nflog_threshold': { + valid: [{ name: '001 test rule', nflog_threshold: 1 }, { name: '001 test rule', nflog_threshold: 313 }], + invalid: [{ name: '001 test rule', nflog_threshold: 'invalid' }, { name: '001 test rule', nflog_threshold: false }, + { name: '001 test rule', nflog_threshold: 0 }] + }, + ':gateway': { + valid: [{ name: '001 test rule', gateway: '10.0.0.2' }, { name: '001 test rule', gateway: '2001:db1::1' }], + invalid: [{ name: '001 test rule', gateway: 'invalid' }, { name: '001 test rule', gateway: false }, + { name: '001 test rule', gateway: false }] + }, + ':clamp_mss_to_pmtu': { + valid: [{ name: '001 test rule', clamp_mss_to_pmtu: true }, { name: '001 test rule', clamp_mss_to_pmtu: false }], + invalid: [{ name: '001 test rule', clamp_mss_to_pmtu: 'invalid' }, { name: '001 test rule', clamp_mss_to_pmtu: 313 }] + }, + ':set_mss': { + valid: [{ name: '001 test rule', set_mss: 1 }, { name: '001 test rule', set_mss: 313 }], + invalid: [{ name: '001 test rule', set_mss: 'invalid' }, { name: '001 test rule', set_mss: false }, + { name: '001 test rule', set_mss: 0 }] + }, + ':set_dscp': { + valid: [{ name: '001 test rule', set_dscp: '0x0a' }], + invalid: [{ name: '001 test rule', set_dscp: '' }, { name: '001 test rule', set_dscp: false }, + { name: '001 test rule', set_dscp: 313 }] + }, + ':set_dscp_class': { + valid: [{ name: '001 test rule', set_dscp_class: 'af11' }, { name: '001 test rule', set_dscp_class: 'cs7' }], + invalid: [{ name: '001 test rule', set_dscp_class: 'invalid' }, { name: '001 test rule', set_dscp_class: false }, + { name: '001 test rule', set_dscp_class: 313 }] + }, + ':todest': { + valid: [{ name: '001 test rule', todest: '10.0.0.2' }, { name: '001 test rule', todest: '10.0.0.2-10.0.0.3' }, + { name: '001 test rule', todest: '10.0.0.2:24' }, { name: '001 test rule', todest: '10.0.0.2-10.0.0.3:24-25' }], + invalid: [{ name: '001 test rule', todest: '' }, { name: '001 test rule', todest: false }, + { name: '001 test rule', todest: 313 }] + }, + ':tosource': { + valid: [{ name: '001 test rule', tosource: '10.0.0.2' }, { name: '001 test rule', tosource: '10.0.0.2-10.0.0.3' }, + { name: '001 test rule', tosource: '10.0.0.2:24' }, { name: '001 test rule', tosource: '10.0.0.2-10.0.0.3:24-25' }], + invalid: [{ name: '001 test rule', tosource: '' }, { name: '001 test rule', tosource: false }, + { name: '001 test rule', tosource: 313 }] + }, + ':toports': { + valid: [{ name: '001 test rule', toports: '40' }, { name: '001 test rule', tosource: '50-60' }], + invalid: [{ name: '001 test rule', toports: 'invalid' }, { name: '001 test rule', toports: false }, + { name: '001 test rule', toports: 313 }] + }, + ':to': { + valid: [{ name: '001 test rule', to: '10.0.0.2' }, { name: '001 test rule', to: '10.0.0.2/24' }], + invalid: [{ name: '001 test rule', to: '' }, { name: '001 test rule', to: false }, + { name: '001 test rule', to: 313 }] + }, + ':checksum_fill': { + valid: [{ name: '001 test rule', checksum_fill: true }, { name: '001 test rule', checksum_fill: false }], + invalid: [{ name: '001 test rule', checksum_fill: 'invalid' }, { name: '001 test rule', checksum_fill: 313 }] + }, + ':random_fully': { + valid: [{ name: '001 test rule', random_fully: true }, { name: '001 test rule', random_fully: false }], + invalid: [{ name: '001 test rule', random_fully: 'invalid' }, { name: '001 test rule', random_fully: 313 }] + }, + ':random': { + valid: [{ name: '001 test rule', random: true }, { name: '001 test rule', random: false }], + invalid: [{ name: '001 test rule', random: 'invalid' }, { name: '001 test rule', random: 313 }] + }, + ':log_prefix': { + valid: [{ name: '001 test rule', log_prefix: 'Prefix Number One' }], + invalid: [{ name: '001 test rule', log_prefix: 313 }, { name: '001 test rule', log_prefix: false }, + { name: '001 test rule', flog_prefix: '' }] + }, + ':log_level': { + valid: [{ name: '001 test rule', log_level: 'warn' }, { name: '001 test rule', log_level: 4 }], + invalid: [{ name: '001 test rule', log_level: 313 }, { name: '001 test rule', log_level: false }, + { name: '001 test rule', log_level: '' }] + }, + ':log_uid': { + valid: [{ name: '001 test rule', log_uid: true }, { name: '001 test rule', log_uid: false }], + invalid: [{ name: '001 test rule', log_uid: 'invalid' }, { name: '001 test rule', log_uid: 313 }] + }, + ':log_tcp_sequence': { + valid: [{ name: '001 test rule', log_tcp_sequence: true }, { name: '001 test rule', log_tcp_sequence: false }], + invalid: [{ name: '001 test rule', log_tcp_sequence: 'invalid' }, { name: '001 test rule', log_tcp_sequence: 313 }] + }, + ':log_tcp_options': { + valid: [{ name: '001 test rule', log_tcp_options: true }, { name: '001 test rule', log_tcp_options: false }], + invalid: [{ name: '001 test rule', log_tcp_options: 'invalid' }, { name: '001 test rule', log_tcp_options: 313 }] + }, + ':log_ip_options': { + valid: [{ name: '001 test rule', log_ip_options: true }, { name: '001 test rule', log_ip_options: false }], + invalid: [{ name: '001 test rule', log_ip_options: 'invalid' }, { name: '001 test rule', log_ip_options: 313 }] + }, + ':reject': { + valid: [{ name: '001 test rule', reject: 'icmp-net-unreachable' }, { name: '001 test rule', reject: 'icmp-proto-unreachable' }, + { name: '001 test rule', reject: 'icmp-admin-prohibited' }, { name: '001 test rule', reject: 'icmp6-port-unreachable' }], + invalid: [{ name: '001 test rule', reject: 'invalid' }, { name: '001 test rule', reject: false }, + { name: '001 test rule', reject: 313 }] + }, + ':set_mark': { + valid: [{ name: '001 test rule', set_mark: '0x3e8' }, { name: '001 test rule', set_mark: '0x3e8/0xffffffff' }], + invalid: [{ name: '001 test rule', set_mark: 'invalid' }, { name: '001 test rule', set_mark: false }, + { name: '001 test rule', set_mark: 313 }] + }, + ':match_mark': { + valid: [{ name: '001 test rule', match_mark: '0x1' }, { name: '001 test rule', match_mark: '! 0x1' }], + invalid: [{ name: '001 test rule', match_mark: 'invalid' }, { name: '001 test rule', match_mark: false }, + { name: '001 test rule', match_mark: 313 }] + }, + ':mss': { + valid: [{ name: '001 test rule', mss: '1361:1541' }, { name: '001 test rule', mss: '! 1361' }, + { name: '001 test rule', mss: '1361:1541' }, { name: '001 test rule', mss: '! 1361:1541' }], + invalid: [{ name: '001 test rule', mss: 'invalid' }, { name: '001 test rule', mss: false }, + { name: '001 test rule', mss: 313 }] + }, + ':connlimit_upto': { + valid: [{ name: '001 test rule', connlimit_upto: 1 }, { name: '001 test rule', connlimit_upto: 313 }], + invalid: [{ name: '001 test rule', connlimit_upto: 'invalid' }, { name: '001 test rule', connlimit_upto: false }] + }, + ':connlimit_above': { + valid: [{ name: '001 test rule', connlimit_above: 1 }, { name: '001 test rule', connlimit_above: 313 }], + invalid: [{ name: '001 test rule', connlimit_above: 'invalid' }, { name: '001 test rule', connlimit_above: false }] + }, + ':connlimit_mask': { + valid: [{ name: '001 test rule', connlimit_mask: 1 }, { name: '001 test rule', connlimit_mask: 128 }], + invalid: [{ name: '001 test rule', connlimit_mask: 'invalid' }, { name: '001 test rule', connlimit_mask: false }, + { name: '001 test rule', connlimit_mask: 313 }] + }, + ':connmark': { + valid: [{ name: '001 test rule', connmark: '0x1' }, { name: '001 test rule', connmark: '! 0x1' }], + invalid: [{ name: '001 test rule', connmark: 'invalid' }, { name: '001 test rule', connmark: false }, + { name: '001 test rule', connmark: 313 }] + }, + ':time_start': { + valid: [{ name: '001 test rule', time_start: '23:59:59' }, { name: '001 test rule', time_start: '03:59' }], + invalid: [{ name: '001 test rule', time_start: '26:70:70' }, { name: '001 test rule', time_start: false }, + { name: '001 test rule', time_start: 313 }] + }, + ':time_stop': { + valid: [{ name: '001 test rule', time_stop: '23:59:59' }, { name: '001 test rule', time_stop: '03:59' }], + invalid: [{ name: '001 test rule', time_stop: '26:70:70' }, { name: '001 test rule', time_stop: false }, + { name: '001 test rule', time_stop: 313 }] + }, + ':month_days': { + valid: [{ name: '001 test rule', month_days: 1 }, { name: '001 test rule', month_days: [3, 1, 3] }], + invalid: [{ name: '001 test rule', month_days: 'invalid' }, { name: '001 test rule', month_days: false }, + { name: '001 test rule', month_days: 313 }, { name: '001 test rule', month_days: [313, 619] }] + }, + ':date_start': { + valid: [{ name: '001 test rule', date_start: '1970-01-01T00:00:00' }, { name: '001 test rule', date_start: '2023-08-08T15:18:00' }], + invalid: [{ name: '001 test rule', date_start: '1690-00-00T70:70:70' }, { name: '001 test rule', date_start: false }, + { name: '001 test rule', date_start: 313 }, { name: '001 test rule', date_start: 'invalid' }] + }, + ':date_stop': { + valid: [{ name: '001 test rule', date_stop: '1970-01-01T00:00:00' }, { name: '001 test rule', date_stop: '2023-08-08T15:18:00' }], + invalid: [{ name: '001 test rule', date_stop: '1690-00-00T70:70:70' }, { name: '001 test rule', date_stop: false }, + { name: '001 test rule', date_stop: 313 }, { name: '001 test rule', date_stop: 'invalid' }] + }, + ':kernel_timezone': { + valid: [{ name: '001 test rule', kernel_timezone: true }, { name: '001 test rule', kernel_timezone: false }], + invalid: [{ name: '001 test rule', kernel_timezone: 'invalid' }, { name: '001 test rule', kernel_timezone: 313 }] + }, + ':u32': { + valid: [{ name: '001 test rule', u32: '0x4&0x1fff=0x0&&0x0&0xf000000=0x5000000' }], + invalid: [{ name: '001 test rule', u32: 'invalid' }, { name: '001 test rule', u32: false }, + { name: '001 test rule', u32: 313 }] + }, + ':src_cc': { + valid: [{ name: '001 test rule', src_cc: 'GB' }, { name: '001 test rule', src_cc: 'GB,US' }], + invalid: [{ name: '001 test rule', src_cc: 'invalid' }, { name: '001 test rule', src_cc: false }, + { name: '001 test rule', src_cc: 313 }] + }, + ':dst_cc': { + valid: [{ name: '001 test rule', dst_cc: 'GB' }, { name: '001 test rule', dst_cc: 'GB,US' }], + invalid: [{ name: '001 test rule', dst_cc: 'invalid' }, { name: '001 test rule', dst_cc: false }, + { name: '001 test rule', dst_cc: 313 }] + }, + ':hashlimit_upto': { + valid: [{ name: '001 test rule', hashlimit_upto: '50/sec' }, { name: '001 test rule', hashlimit_upto: '40/min' }, + { name: '001 test rule', hashlimit_upto: '30/hour' }, { name: '001 test rule', hashlimit_upto: '10/day' }], + invalid: [{ name: '001 test rule', hashlimit_upto: true }, { name: '001 test rule', hashlimit_upto: 30 }, + { name: '001 test rule', hashlimit_upto: 'invalid' }] + }, + ':hashlimit_above': { + valid: [{ name: '001 test rule', hashlimit_above: '50/sec' }, { name: '001 test rule', hashlimit_above: '40/min' }, + { name: '001 test rule', hashlimit_above: '30/hour' }, { name: '001 test rule', hashlimit_above: '10/day' }], + invalid: [{ name: '001 test rule', hashlimit_above: true }, { name: '001 test rule', hashlimit_above: 30 }, + { name: '001 test rule', hashlimit_above: 'invalid' }] + }, + ':hashlimit_name': { + valid: [{ name: '001 test rule', hashlimit_name: 'above' }, { name: '001 test rule', hashlimit_name: 'upto' }], + invalid: [{ name: '001 test rule', hashlimit_name: true }, { name: '001 test rule', hashlimit_name: 30 }, + { name: '001 test rule', hashlimit_name: '' }] + }, + ':hashlimit_burst': { + valid: [{ name: '001 test rule', hashlimit_burst: 1 }, { name: '001 test rule', hashlimit_burst: 313 }], + invalid: [{ name: '001 test rule', hashlimit_burst: 'invalid' }, { name: '001 test rule', hashlimit_burst: false }] + }, + ':hashlimit_mode': { + valid: [{ name: '001 test rule', hashlimit_mode: 'srcip' }, { name: '001 test rule', hashlimit_mode: 'srcip,srcport,dstip,dstport' }], + invalid: [{ name: '001 test rule', hashlimit_mode: true }, { name: '001 test rule', hashlimit_mode: 30 }, + { name: '001 test rule', hashlimit_mode: 'invalid' }] + }, + ':hashlimit_srcmask': { + valid: [{ name: '001 test rule', hashlimit_srcmask: 1 }, { name: '001 test rule', hashlimit_srcmask: 32 }], + invalid: [{ name: '001 test rule', hashlimit_srcmask: 'invalid' }, { name: '001 test rule', hashlimit_srcmask: false }, + { name: '001 test rule', hashlimit_mode: 33 }] + }, + ':hashlimit_dstmask': { + valid: [{ name: '001 test rule', hashlimit_dstmask: 1 }, { name: '001 test rule', hashlimit_dstmask: 32 }], + invalid: [{ name: '001 test rule', hashlimit_dstmask: 'invalid' }, { name: '001 test rule', hashlimit_dstmask: false }, + { name: '001 test rule', hashlimit_dstmask: 33 }] + }, + ':hashlimit_htable_size': { + valid: [{ name: '001 test rule', hashlimit_htable_size: 1 }, { name: '001 test rule', hashlimit_htable_size: 313 }], + invalid: [{ name: '001 test rule', hashlimit_htable_size: 'invalid' }, { name: '001 test rule', hashlimit_htable_size: false }] + }, + ':hashlimit_htable_max': { + valid: [{ name: '001 test rule', hashlimit_htable_max: 1 }, { name: '001 test rule', hashlimit_htable_max: 313 }], + invalid: [{ name: '001 test rule', hashlimit_htable_max: 'invalid' }, { name: '001 test rule', hashlimit_htable_max: false }] + }, + ':hashlimit_htable_expire': { + valid: [{ name: '001 test rule', hashlimit_htable_expire: 1 }, { name: '001 test rule', hashlimit_htable_expire: 313 }], + invalid: [{ name: '001 test rule', hashlimit_htable_expire: 'invalid' }, { name: '001 test rule', hashlimit_htable_expire: false }] + }, + ':hashlimit_htable_gcinterval': { + valid: [{ name: '001 test rule', hashlimit_htable_gcinterval: 1 }, { name: '001 test rule', hashlimit_htable_gcinterval: 313 }], + invalid: [{ name: '001 test rule', hashlimit_htable_gcinterval: 'invalid' }, { name: '001 test rule', hashlimit_htable_gcinterval: false }] + }, + ':bytecode': { + valid: [{ name: '001 test rule', bytecode: '4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0' }], + invalid: [{ name: '001 test rule', bytecode: 313 }, { name: '001 test rule', bytecode: false }, + { name: '001 test rule', bytecode: '' }] + }, + ':ipvs': { + valid: [{ name: '001 test rule', ipvs: true }, { name: '001 test rule', ipvs: false }], + invalid: [{ name: '001 test rule', ipvs: 'invalid' }, { name: '001 test rule', ipvs: 313 }] + }, + ':zone': { + valid: [{ name: '001 test rule', zone: 1 }, { name: '001 test rule', zone: 313 }], + invalid: [{ name: '001 test rule', zone: 'invalid' }, { name: '001 test rule', zone: false }] + }, + ':helper': { + valid: [{ name: '001 test rule', helper: 'helperOne' }], + invalid: [{ name: '001 test rule', helper: 313 }, { name: '001 test rule', helper: false }, + { name: '001 test rule', helper: '' }] + }, + ':cgroup': { + valid: [{ name: '001 test rule', cgroup: '0x100001' }], + invalid: [{ name: '001 test rule', cgroup: 313 }, { name: '001 test rule', cgroup: false }, + { name: '001 test rule', cgroup: '' }] + }, + ':rpfilter': { + valid: [{ name: '001 test rule', rpfilter: 'loose' }, { name: '001 test rule', rpfilter: 'accept-local' }, + { name: '001 test rule', rpfilter: ['validmark', 'invert'] }], + invalid: [{ name: '001 test rule', rpfilter: 'invalid' }, { name: '001 test rule', rpfilter: false }, + { name: '001 test rule', rpfilter: false }] + }, + ':condition': { + valid: [{ name: '001 test rule', condition: 'isblue' }, { name: '001 test rule', condition: '! isblue' }], + invalid: [{ name: '001 test rule', condition: 313 }, { name: '001 test rule', condition: false }, + { name: '001 test rule', condition: '' }] + }, + ':notrack': { + valid: [{ name: '001 test rule', notrack: true }, { name: '001 test rule', notrack: false }], + invalid: [{ name: '001 test rule', notrack: 'invalid' }, { name: '001 test rule', notrack: 313 }] } - icmp_codes.each do |provider, values| - describe provider do - values.each do |k, v| - resource_type = [:provider, :icmp] - resource_value = [provider, v] - resource_expected = [provider, k] - it 'converts icmp string to number' do - resource_type.each_with_index do |type, index| - resource[type] = resource_value[index] - expect(resource[type]).to eql resource_expected[index] - end + }.each do |attribute| + describe attribute[0] do + context 'when given a valid value' do + attribute[1][:valid].each do |valid_input| + it valid_input do + expect { firewall.new(valid_input) }.not_to raise_error end end end - end - - it 'accepts values as integers' do - resource[:icmp] = 9 - expect(resource[:icmp]).to be 9 - end - - it 'fails if icmp type is "any"' do - expect(-> { resource[:icmp] = 'any' }).to raise_error(Puppet::Error) - end - it 'fails if icmp type is an array' do - expect(-> { resource[:icmp] = "['0', '8']" }).to raise_error(Puppet::Error) - end - - it 'fails if icmp type cannot be mapped to a numeric' do - expect(-> { resource[:icmp] = 'foo' }).to raise_error(Puppet::Error) - end - end - describe ':state' do - it 'accepts value as a string - INVALID' do - resource[:state] = :INVALID - expect(resource[:state]).to eql [:INVALID] - end - - it 'accepts value as a string - UNTRACKED' do - resource[:state] = :UNTRACKED - expect(resource[:state]).to eql [:UNTRACKED] - end - - it 'accepts value as an array - INVALID, NEW' do - resource[:state] = [:INVALID, :NEW] - expect(resource[:state]).to eql [:INVALID, :NEW] - end - - it 'sorts values alphabetically - NEW, UNTRACKED, ESTABLISHED' do - resource[:state] = [:NEW, :UNTRACKED, :ESTABLISHED] - expect(resource[:state]).to eql [:ESTABLISHED, :NEW, :UNTRACKED] - end - end - - describe ':ctstate' do - it 'accepts value as a string - INVALID' do - resource[:ctstate] = :INVALID - expect(resource[:ctstate]).to eql [:INVALID] - end - - it 'accepts value as a string - UNTRACKED' do - resource[:state] = :UNTRACKED - expect(resource[:state]).to eql [:UNTRACKED] - end - - it 'accepts value as an array - INVALID, NEW' do - resource[:ctstate] = [:INVALID, :NEW] - expect(resource[:ctstate]).to eql [:INVALID, :NEW] - end - - it 'sorts values alphabetically - NEW, ESTABLISHED' do - resource[:ctstate] = [:NEW, :ESTABLISHED] - expect(resource[:ctstate]).to eql [:ESTABLISHED, :NEW] - end - end - - describe ':ctproto' do - it 'accepts numeric value' do - resource[:ctproto] = 6 - expect(resource[:ctproto]).to be 6 - end - it 'accepts negated string value' do - resource[:ctproto] = '! 6' - expect(resource[:ctproto]).to eql '! 6' - end - end - - [:ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst].each do |addr| - describe addr do - it "accepts a #{addr} as a string without /32" do - resource[addr] = '127.0.0.1' - expect(resource[addr]).to eql '127.0.0.1' - end - it "accepts a #{addr} as a string with /32" do - resource[addr] = '127.0.0.1/32' - expect(resource[addr]).to eql '127.0.0.1' - end - it "accepts a #{addr} as a string with cidr" do - resource[addr] = '10.0.0.0/8' - expect(resource[addr]).to eql '10.0.0.0/8' - end - it "accepts a #{addr} as a string with ipv6 cidr" do - resource[addr] = '2001:DB8::/64' - expect(resource[addr]).to eql '2001:DB8::/64' - end - it "accepts a negated #{addr} as a string" do - resource[addr] = '! 127.0.0.1' - expect(resource[addr]).to eql '! 127.0.0.1' - end - it "accepts a negated #{addr} as a string with cidr" do - resource[addr] = '! 10.0.0.0/8' - expect(resource[addr]).to eql '! 10.0.0.0/8' - end - end - end - - [:ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport].each do |port| - describe port do - it "accepts #{port} as numeric value" do - resource[port] = 80 - expect(resource[port]).to be 80 - end - it "accepts #{port} as range value" do - resource[port] = '80:81' - expect(resource[port]).to eql '80:81' - end - it "accepts a negated #{port} as string value" do - resource[port] = '! 80' - expect(resource[port]).to eql '! 80' - end - it "accepts a negated #{port} as range value" do - resource[port] = '! 80:81' - expect(resource[port]).to eql '! 80:81' - end - end - end - - describe ':ctstatus' do - it 'accepts value as a string - EXPECTED' do - resource[:ctstatus] = :EXPECTED - expect(resource[:ctstatus]).to eql [:EXPECTED] - end - - it 'accepts value as an array - EXPECTED, SEEN_REPLY' do - resource[:ctstatus] = [:EXPECTED, :SEEN_REPLY] - expect(resource[:ctstatus]).to eql [:EXPECTED, :SEEN_REPLY] - end - - it 'sorts values alphabetically - SEEN_REPLY, EXPECTED' do - resource[:ctstatus] = [:SEEN_REPLY, :EXPECTED] - expect(resource[:ctstatus]).to eql [:EXPECTED, :SEEN_REPLY] - end - end - - describe ':ctexpire' do - it 'accepts numeric values' do - resource[:ctexpire] = 100 - expect(resource[:ctexpire]).to be 100 - end - - it 'accepts numeric range values' do - resource[:ctexpire] = '100:120' - expect(resource[:ctexpire]).to eql '100:120' - end - end - - describe ':ctdir' do - it 'accepts value as a string - REPLY' do - resource[:ctdir] = :REPLY - expect(resource[:ctdir]).to be :REPLY - end - - it 'accepts value as a string - ORIGINAL' do - resource[:ctdir] = :ORIGINAL - expect(resource[:ctdir]).to be :ORIGINAL - end - end - - describe ':burst' do - it 'accepts numeric values' do - resource[:burst] = 12 - expect(resource[:burst]).to be 12 - end - - it 'fails if value is not numeric' do - expect(-> { resource[:burst] = 'foo' }).to raise_error(Puppet::Error) - end - it 'fails if value contains /sec' do - expect(-> { resource[:burst] = '1500/sec' }).to raise_error(Puppet::Error) - end - end - - describe ':recent' do - ['set', 'update', 'rcheck', 'remove'].each do |recent| - it "accepts recent value #{recent}" do - resource[:recent] = recent - expect(resource[:recent]).to eql "--#{recent}" - end - end - end - - describe ':action and :jump' do - it 'allows only 1 to be set at a time' do - expect { - firewall_class.new(name: '001-test', action: 'accept', jump: 'custom_chain') - }.to raise_error(RuntimeError, %r{Only one of the parameters 'action' and 'jump' can be set$}) - end - end - - describe ':gid and :uid' do - it 'allows me to set uid' do - resource[:uid] = 'root' - expect(resource[:uid]).to eql 'root' - end - it 'allows me to set uid as an array, and silently hide my error' do - resource[:uid] = ['root', 'bobby'] - expect(resource[:uid]).to eql 'root' - end - it 'allows me to set gid' do - resource[:gid] = 'root' - expect(resource[:gid]).to eql 'root' - end - it 'allows me to set gid as an array, and silently hide my error' do - resource[:gid] = ['root', 'bobby'] - expect(resource[:gid]).to eql 'root' - end - end - - describe ':set_mark' do - ['1.3.2', '1.4.2'].each do |iptables_version| - describe "with iptables #{iptables_version}" do - before(:each) do - Facter.clear - allow(Facter.fact(:iptables_version)).to receive(:value).and_return iptables_version - allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return iptables_version - end - - if iptables_version == '1.3.2' - it 'allows me to set set-mark without mask' do - resource[:set_mark] = '0x3e8' - expect(resource[:set_mark]).to eql '0x3e8' - end - it 'converts int to hex without mask' do - resource[:set_mark] = '1000' - expect(resource[:set_mark]).to eql '0x3e8' - end - it 'fails if mask is present' do - expect(-> { resource[:set_mark] = '0x3e8/0xffffffff' }).to raise_error( - Puppet::Error, %r{iptables version #{iptables_version} does not support masks on MARK rules$} - ) + context 'when given an invalid value' do + attribute[1][:invalid].each do |invalid_input| + it invalid_input do + expect { firewall.new(invalid_input) }.to raise_error(Puppet::Error) end end - - if iptables_version == '1.4.2' - it 'allows me to set set-mark with mask' do - resource[:set_mark] = '0x3e8/0xffffffff' - expect(resource[:set_mark]).to eql '0x3e8/0xffffffff' - end - it 'converts int to hex and add a 32 bit mask' do - resource[:set_mark] = '1000' - expect(resource[:set_mark]).to eql '0x3e8/0xffffffff' - end - it 'adds a 32 bit mask' do - resource[:set_mark] = '0x32' - expect(resource[:set_mark]).to eql '0x32/0xffffffff' - end - it 'uses the mask provided' do - resource[:set_mark] = '0x32/0x4' - expect(resource[:set_mark]).to eql '0x32/0x4' - end - it 'uses the mask provided and convert int to hex' do - resource[:set_mark] = '1000/0x4' - expect(resource[:set_mark]).to eql '0x3e8/0x4' - end - it 'fails if mask value is more than 32 bits' do - expect(-> { resource[:set_mark] = '1/4294967296' }).to raise_error( - Puppet::Error, %r{MARK mask must be integer or hex between 0 and 0xffffffff$} - ) - end - it 'fails if mask is malformed' do - expect(-> { resource[:set_mark] = '1000/0xq4' }).to raise_error( - Puppet::Error, %r{MARK mask must be integer or hex between 0 and 0xffffffff$} - ) - end - end - - ['/', '1000/', 'pwnie'].each do |bad_mark| - it "fails with malformed mark '#{bad_mark}'" do - expect(-> { resource[:set_mark] = bad_mark }).to raise_error(Puppet::Error) - end - end - it 'fails if mark value is more than 32 bits' do - expect(-> { resource[:set_mark] = '4294967296' }).to raise_error( - Puppet::Error, %r{MARK value must be integer or hex between 0 and 0xffffffff$} - ) - end - end - end - end - - describe 'ct_target' do - it 'allows me to set zone' do - resource[:zone] = 4000 - expect(resource[:zone]).to be 4000 - end - end - - [:chain, :jump].each do |param| - describe param do - it 'autorequires fwchain when table and provider are undefined' do - resource[param] = 'FOO' - expect(resource[:table]).to be :filter - expect(resource[:provider]).to be :iptables - - chain = Puppet::Type.type(:firewallchain).new(name: 'FOO:filter:IPv4') - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain - rel = resource.autorequire[0] - expect(rel.source.ref).to eql chain.ref - expect(rel.target.ref).to eql resource.ref - end - - it 'autorequires fwchain when table is undefined and provider is ip6tables' do - resource[param] = 'FOO' - expect(resource[:table]).to be :filter - resource[:provider] = :ip6tables - - chain = Puppet::Type.type(:firewallchain).new(name: 'FOO:filter:IPv6') - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain - rel = resource.autorequire[0] - expect(rel.source.ref).to eql chain.ref - expect(rel.target.ref).to eql resource.ref - end - - it 'autorequires fwchain when table is raw and provider is undefined' do - resource[param] = 'FOO' - resource[:table] = :raw - expect(resource[:provider]).to be :iptables - - chain = Puppet::Type.type(:firewallchain).new(name: 'FOO:raw:IPv4') - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain - rel = resource.autorequire[0] - expect(rel.source.ref).to eql chain.ref - expect(rel.target.ref).to eql resource.ref - end - - it 'autorequires fwchain when table is raw and provider is ip6tables' do - resource[param] = 'FOO' - resource[:table] = :raw - resource[:provider] = :ip6tables - - chain = Puppet::Type.type(:firewallchain).new(name: 'FOO:raw:IPv6') - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain - rel = resource.autorequire[0] - expect(rel.source.ref).to eql chain.ref - expect(rel.target.ref).to eql resource.ref - end - - # test where autorequire is still needed (table != filter) - ['INPUT', 'OUTPUT', 'FORWARD'].each do |test_chain| - it "autorequires fwchain #{test_chain} when table is mangle and provider is undefined" do - resource[param] = test_chain - resource[:table] = :mangle - expect(resource[:provider]).to be :iptables - - chain = Puppet::Type.type(:firewallchain).new(name: "#{test_chain}:mangle:IPv4") - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain - rel = resource.autorequire[0] - expect(rel.source.ref).to eql chain.ref - expect(rel.target.ref).to eql resource.ref - end - - it "autorequires fwchain #{test_chain} when table is mangle and provider is ip6tables" do - resource[param] = test_chain - resource[:table] = :mangle - resource[:provider] = :ip6tables - - chain = Puppet::Type.type(:firewallchain).new(name: "#{test_chain}:mangle:IPv6") - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain - rel = resource.autorequire[0] - expect(rel.source.ref).to eql chain.ref - expect(rel.target.ref).to eql resource.ref - end - end - - # test of case where autorequire should not happen - ['INPUT', 'OUTPUT', 'FORWARD'].each do |test_chain| - it "does not autorequire fwchain #{test_chain} when table and provider are undefined" do - resource[param] = test_chain - expect(resource[:table]).to be :filter - expect(resource[:provider]).to be :iptables - - chain = Puppet::Type.type(:firewallchain).new(name: "#{test_chain}:filter:IPv4") - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain - rel = resource.autorequire[0] - expect(rel).to be nil - end - - it "does not autorequire fwchain #{test_chain} when table is undefined and provider is ip6tables" do - resource[param] = test_chain - expect(resource[:table]).to be :filter - resource[:provider] = :ip6tables - - chain = Puppet::Type.type(:firewallchain).new(name: "#{test_chain}:filter:IPv6") - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain - rel = resource.autorequire[0] - expect(rel).to be nil - end - end - end - end - - describe ':chain and :jump' do - it 'autorequires independent fwchains' do - resource[:chain] = 'FOO' - resource[:jump] = 'BAR' - expect(resource[:table]).to be :filter - expect(resource[:provider]).to be :iptables - - chain_foo = Puppet::Type.type(:firewallchain).new(name: 'FOO:filter:IPv4') - chain_bar = Puppet::Type.type(:firewallchain).new(name: 'BAR:filter:IPv4') - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource chain_foo - catalog.add_resource chain_bar - rel = resource.autorequire - expect(rel[0].source.ref).to eql chain_foo.ref - expect(rel[0].target.ref).to eql resource.ref - expect(rel[1].source.ref).to eql chain_bar.ref - expect(rel[1].target.ref).to eql resource.ref - end - end - # rubocop:enable RSpec/ExampleLength - # rubocop:enable RSpec/MultipleExpectations - - describe ':pkttype' do - [:multicast, :broadcast, :unicast].each do |pkttype| - it "accepts pkttype value #{pkttype}" do - resource[:pkttype] = pkttype - expect(resource[:pkttype]).to eql pkttype end end - - it 'fails when the pkttype value is not recognized' do - expect(-> { resource[:pkttype] = 'not valid' }).to raise_error(Puppet::Error) - end - end - - describe ':condition' do - it 'accepts value as a string' do - resource[:condition] = 'somefile' - expect(resource[:condition]).to eq('somefile') - end - end - - describe 'autorequire packages' do - [:iptables, :ip6tables].each do |provider| - it "provider #{provider} should autorequire package iptables" do - resource[:provider] = provider - expect(resource[:provider]).to eql provider - package = Puppet::Type.type(:package).new(name: 'iptables') - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource package - rel = resource.autorequire[0] - expect(rel.source.ref).to eql package.ref - expect(rel.target.ref).to eql resource.ref - end - - it "provider #{provider} should autorequire packages iptables, iptables-persistent, and iptables-services" do - resource[:provider] = provider - expect(resource[:provider]).to eql provider - packages = [ - Puppet::Type.type(:package).new(name: 'iptables'), - Puppet::Type.type(:package).new(name: 'iptables-persistent'), - Puppet::Type.type(:package).new(name: 'iptables-services'), - ] - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - packages.each do |package| - catalog.add_resource package - end - packages.zip(resource.autorequire) do |package, rel| - expect(rel.source.ref).to eql package.ref - expect(rel.target.ref).to eql resource.ref - end - end - end - # rubocop:enable RSpec/ExampleLength - # rubocop:enable RSpec/MultipleExpectations - end - it 'is suitable' do - expect(resource).to be_suitable - end -end - -describe 'firewall on unsupported platforms' do - it 'is not suitable' do - # Stub iptables version - allow(Facter.fact(:iptables_version)).to receive(:value).and_return(nil) - allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return(nil) - - # Stub confine facts - allow(Facter.fact(:kernel)).to receive(:value).and_return('Darwin') - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Darwin') - resource = firewall.new(name: '000 test foo', ensure: :present) - - # If our provider list is nil, then the Puppet::Transaction#evaluate will - # say 'Error: Could not find a suitable provider for firewall' but there - # isn't a unit testable way to get this. - expect(resource).not_to be_suitable end end diff --git a/spec/unit/puppet/type/firewallchain_spec.rb b/spec/unit/puppet/type/firewallchain_spec.rb old mode 100755 new mode 100644 index 3006776ed..d23badb91 --- a/spec/unit/puppet/type/firewallchain_spec.rb +++ b/spec/unit/puppet/type/firewallchain_spec.rb @@ -1,213 +1,126 @@ -#!/usr/bin/env rspec # frozen_string_literal: true require 'spec_helper' +require 'puppet/type/firewallchain' -firewallchain = Puppet::Type.type(:firewallchain) +RSpec.describe 'firewallchain type' do + let(:firewallchain) { Puppet::Type.type(:firewallchain) } -describe firewallchain do # rubocop:disable RSpec/MultipleDescribes - before(:each) do - # Stub confine facts - allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux') - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Debian') - end - let(:klass) { firewallchain } - let(:provider) do - prov = instance_double('provider') - allow(prov).to receive(:name).and_return(:iptables_chain) - prov - end - let(:resource) do - allow(Puppet::Type::Firewallchain).to receive(:defaultprovider).and_return provider - klass.new(name: 'INPUT:filter:IPv4', policy: :accept) + it 'loads' do + expect(firewallchain).not_to be_nil end it 'has :name be its namevar' do - expect(klass.key_attributes).to eql [:name] + expect(firewallchain.key_attributes).to eql [:name] end - describe ':name' do - { 'nat' => ['PREROUTING', 'POSTROUTING', 'INPUT', 'OUTPUT'], - 'mangle' => ['PREROUTING', 'POSTROUTING', 'INPUT', 'FORWARD', 'OUTPUT'], - 'filter' => ['INPUT', 'OUTPUT', 'FORWARD'], - 'raw' => ['PREROUTING', 'OUTPUT'], - 'broute' => ['BROUTING'], - 'security' => ['INPUT', 'OUTPUT', 'FORWARD'] }.each_pair do |table, allowedinternalchains| - ['IPv4', 'IPv6', 'ethernet'].each do |protocol| - ['test', '$5()*&%\'"^$09):'].each do |chainname| - name = "#{chainname}:#{table}:#{protocol}" - if table == 'nat' && protocol == 'IPv6' - it "accepts #{name} for Linux 3.7+" do - allow(Facter.fact(:kernelmajversion)).to receive(:value).and_return('3.7') - resource[:name] = name - expect(resource[:name]).to eql name - end - it "fails #{name} for Linux 2.6" do - allow(Facter.fact(:kernelmajversion)).to receive(:value).and_return('2.6') - expect { resource[:name] = name }.to raise_error(Puppet::Error) - end - elsif protocol != 'ethernet' && table == 'broute' - it "fails #{name}" do # rubocop:disable RSpec/RepeatedExample,RSpec/RepeatedDescription - expect { resource[:name] = name }.to raise_error(Puppet::Error) - end - else - it "accepts name #{name}" do # rubocop:disable RSpec/RepeatedExample - resource[:name] = name - expect(resource[:name]).to eql name - end - end + describe ':ensure' do + context 'when given valid input' do + ['present', 'absent'].each do |input| + it input do + expect { firewallchain.new(name: 'INPUT:filter:IPv4', ensure: input) }.not_to raise_error end end + end - ['PREROUTING', 'POSTROUTING', 'BROUTING', 'INPUT', 'FORWARD', 'OUTPUT'].each do |internalchain| - name = internalchain + ':' + table + ':' - name += if internalchain == 'BROUTING' - 'ethernet' - elsif table == 'nat' - 'IPv4' - else - 'IPv4' - end - if allowedinternalchains.include? internalchain - it "allows #{name}" do # rubocop:disable RSpec/RepeatedExample - resource[:name] = name - expect(resource[:name]).to eql name - end - else - it "fails #{name}" do # rubocop:disable RSpec/RepeatedExample,RSpec/RepeatedDescription - expect { resource[:name] = name }.to raise_error(Puppet::Error) - end + context 'when given invalid input' do + [true, 123, 'false'].each do |input| + it input do + expect { firewallchain.new(name: 'INPUT:filter:IPv4', ensure: input) }.to raise_error(Puppet::Error) end end end + end - it 'fails with invalid table names' do - expect { resource[:name] = 'wrongtablename:test:IPv4' }.to raise_error(Puppet::Error) + describe ':name' do + context 'when given valid input' do + ['INPUT:filter:IPv4', 'FORWARD:mangle:IPv6', 'PREROUTING:nat:IPv4', 'INPUT:filter:IPv6', 'OUTPUT:raw:IPv6', + 'BROUTING:broute:ethernet', 'test_chain:security:IPv4'].each do |input| + it input do + expect { firewallchain.new(name: input) }.not_to raise_error + end + end end - it 'fails with invalid protocols names' do - expect { resource[:name] = 'test:filter:IPv5' }.to raise_error(Puppet::Error) + context 'when given invalid input' do + ['INPUT:filter:IPv', 'FORWARD:glee:IPv6', 'PREROUTING:nat:iptables', 'INPUT:filter:Iv6', true, + 123, ':glee:IPv6'].each do |input| + it input do + expect { firewallchain.new(name: input) }.to raise_error(Puppet::Error) + end + end end end - describe ':policy' do - [:accept, :drop, :queue, :return].each do |policy| - it "accepts policy #{policy}" do - resource[:policy] = policy - expect(resource[:policy]).to eql policy + describe 'policy' do + context 'when given valid input' do + ['accept', 'drop', 'queue', 'return'].each do |input| + it input do + expect { firewallchain.new(name: 'INPUT:filter:IPv4', policy: input) }.not_to raise_error + end end end - it 'fails when value is not recognized' do - expect { resource[:policy] = 'not valid' }.to raise_error(Puppet::Error) - end - - [:accept, :drop, :queue, :return].each do |policy| - it "non-inbuilt chains should not accept policy #{policy}" do - expect { klass.new(name: 'testchain:filter:IPv4', policy: policy) }.to raise_error(RuntimeError) - end - it "non-inbuilt chains can accept policies on protocol = ethernet (policy #{policy})" do - klass.new(name: 'testchain:filter:ethernet', policy: policy) + context 'when given invalid input' do + ['acquise', true, 123].each do |input| + it input do + expect { firewallchain.new(name: 'INPUT:filter:IPv4', policy: input) }.to raise_error(Puppet::Error) + end end end end - describe 'autorequire packages' do - it 'provider iptables_chain should autorequire package iptables' do - expect(resource[:provider]).to be :iptables_chain - package = Puppet::Type.type(:package).new(name: 'iptables') - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - catalog.add_resource package - rel = resource.autorequire[0] - expect(rel.source.ref).to eql package.ref - expect(rel.target.ref).to eql resource.ref + describe 'purge' do + context 'when given valid input' do + [true, false].each do |input| + it input do + expect { firewallchain.new(name: 'INPUT:filter:IPv4', purge: input) }.not_to raise_error + end + end end - it 'provider iptables_chain should autorequire packages iptables, iptables-persistent, and iptables-services' do - expect(resource[:provider]).to be :iptables_chain - packages = [ - Puppet::Type.type(:package).new(name: 'iptables'), - Puppet::Type.type(:package).new(name: 'iptables-persistent'), - Puppet::Type.type(:package).new(name: 'iptables-services'), - ] - catalog = Puppet::Resource::Catalog.new - catalog.add_resource resource - packages.each do |package| - catalog.add_resource package - end - packages.zip(resource.autorequire) do |package, rel| - expect(rel.source.ref).to eql package.ref - expect(rel.target.ref).to eql resource.ref + context 'when given invalid input' do + ['true', 'false', 123].each do |input| + it input do + expect { firewallchain.new(name: 'INPUT:filter:IPv4', purge: input) }.to raise_error(Puppet::Error) + end end end end - describe 'purge iptables rules' do - before(:each) do - stub_return = < /etc/iptables/iptables.rules']) - host.persist_iptables(proto) - end - - it 'is expected to raise a warning when exec fails' do - allow(Facter.fact(:osfamily)).to receive(:value).and_return('RedHat') - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('RedHat') - allow(Facter.fact(:operatingsystemrelease)).to receive(:value).and_return('6') - - allow(host).to receive(:execute).with(['/sbin/service', 'iptables', 'save']).and_raise(Puppet::ExecutionFailure, 'some error') - allow(host).to receive(:warning).with('Unable to persist firewall rules: some error') - host.persist_iptables(proto) - end - end - - describe 'when proto is IPv6' do - let(:proto) { 'IPv6' } - - it 'is expected to exec for newer Ubuntu' do - allow(Facter.fact(:osfamily)).to receive(:value).and_return(nil) - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Ubuntu') - allow(Facter.fact(:iptables_persistent_version)).to receive(:value).and_return('0.5.3ubuntu2') - allow(host).to receive(:execute).with(['/usr/sbin/service', 'iptables-persistent', 'save']) - host.persist_iptables(proto) - end - - it 'is expected to not exec for older Ubuntu which does not support IPv6' do - allow(Facter.fact(:osfamily)).to receive(:value).and_return(nil) - allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Ubuntu') - allow(Facter.fact(:iptables_persistent_version)).to receive(:value).and_return('0.0.20090701') - allow(host).to receive(:execute).never - host.persist_iptables(proto) - end - - it 'is expected to not exec for Suse which is not supported' do - allow(Facter.fact(:osfamily)).to receive(:value).and_return('Suse') - allow(host).to receive(:execute).never - host.persist_iptables(proto) - end - end - # rubocop:enable RSpec/SubjectStub - end -end diff --git a/spec/unit/puppet/util/ipcidr_spec.rb b/spec/unit/puppet_x/puppetlabs/firewall/ipcidr_spec.rb similarity index 67% rename from spec/unit/puppet/util/ipcidr_spec.rb rename to spec/unit/puppet_x/puppetlabs/firewall/ipcidr_spec.rb index b57fa354a..b893dd1de 100644 --- a/spec/unit/puppet/util/ipcidr_spec.rb +++ b/spec/unit/puppet_x/puppetlabs/firewall/ipcidr_spec.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true +require 'puppet_x' require 'spec_helper' -require 'puppet/util/ipcidr' +require 'puppet_x/puppetlabs/firewall/ipcidr' + +RSpec.describe PuppetX::Firewall::IPCidr do # rubocop:disable RSpec/FilePath + let(:ipcidr) { described_class } -describe 'Puppet::Util::IPCidr' do describe 'ipv4 address' do subject(:host) { ipaddr } - let(:ipaddr) { Puppet::Util::IPCidr.new('96.126.112.51') } + let(:ipaddr) { ipcidr.new('96.126.112.51') } it { expect(host.cidr).to eql '96.126.112.51/32' } it { expect(host.prefixlen).to be 32 } @@ -15,9 +18,9 @@ end describe 'single ipv4 address with cidr' do - subject(:host) { ipcidr } + subject(:host) { ipaddr } - let(:ipcidr) { Puppet::Util::IPCidr.new('96.126.112.51/32') } + let(:ipaddr) { ipcidr.new('96.126.112.51/32') } it { expect(host.cidr).to eql '96.126.112.51/32' } it { expect(host.prefixlen).to be 32 } @@ -25,9 +28,9 @@ end describe 'ipv4 address range with cidr' do - subject(:host) { ipcidr } + subject(:host) { ipaddr } - let(:ipcidr) { Puppet::Util::IPCidr.new('96.126.112.0/24') } + let(:ipaddr) { ipcidr.new('96.126.112.0/24') } it { expect(host.cidr).to eql '96.126.112.0/24' } it { expect(host.prefixlen).to be 24 } @@ -36,20 +39,19 @@ # https://tickets.puppetlabs.com/browse/MODULES-3215 describe 'ipv4 address range with invalid cidr' do - subject(:host) { ipcidr } + subject(:host) { ipaddr } - let(:ipcidr) { Puppet::Util::IPCidr.new('96.126.112.20/24') } + let(:ipaddr) { ipcidr.new('96.126.112.20/24') } - specify { host.cidr.should == '96.126.112.0/24' } # .20 is expected to - # be silently dropped. - specify { host.prefixlen.should == 24 } - specify { host.netmask.should == '255.255.255.0' } + it { expect(host.cidr).to eq '96.126.112.0/24' } # .20 is expected to be silently dropped. + it { expect(host.prefixlen).to be 24 } + it { expect(host.netmask).to eql '255.255.255.0' } end describe 'ipv4 open range with cidr' do - subject(:host) { ipcidr } + subject(:host) { ipaddr } - let(:ipcidr) { Puppet::Util::IPCidr.new('0.0.0.0/0') } + let(:ipaddr) { ipcidr.new('0.0.0.0/0') } it { expect(host.cidr).to eql '0.0.0.0/0' } it { expect(host.prefixlen).to be 0 } @@ -57,7 +59,7 @@ end describe 'ipv4 invalid address' do - subject(:host) { Puppet::Util::IPCidr.new('256.168.2.0/24') } + subject(:host) { ipcidr.new('256.168.2.0/24') } it { expect { host }.to raise_error ArgumentError, %r{256.168.2.0/24} } end @@ -65,7 +67,7 @@ describe 'ipv6 address' do subject(:host) { ipaddr } - let(:ipaddr) { Puppet::Util::IPCidr.new('2001:db8:85a3:0:0:8a2e:370:7334') } + let(:ipaddr) { ipcidr.new('2001:db8:85a3:0:0:8a2e:370:7334') } it { expect(host.cidr).to eql '2001:db8:85a3::8a2e:370:7334/128' } it { expect(host.prefixlen).to be 128 } @@ -75,7 +77,7 @@ describe 'single ipv6 addr with cidr' do subject(:host) { ipaddr } - let(:ipaddr) { Puppet::Util::IPCidr.new('2001:db8:85a3:0:0:8a2e:370:7334/128') } + let(:ipaddr) { ipcidr.new('2001:db8:85a3:0:0:8a2e:370:7334/128') } it { expect(host.cidr).to eql '2001:db8:85a3::8a2e:370:7334/128' } it { expect(host.prefixlen).to be 128 } @@ -85,7 +87,7 @@ describe 'ipv6 addr range with cidr' do subject(:host) { ipaddr } - let(:ipaddr) { Puppet::Util::IPCidr.new('2001:db8:1234::/48') } + let(:ipaddr) { ipcidr.new('2001:db8:1234::/48') } it { expect(host.cidr).to eql '2001:db8:1234::/48' } it { expect(host.prefixlen).to be 48 } @@ -95,7 +97,7 @@ describe 'ipv6 open range with cidr' do subject(:host) { ipaddr } - let(:ipaddr) { Puppet::Util::IPCidr.new('::/0') } + let(:ipaddr) { ipcidr.new('::/0') } it { expect(host.cidr).to eql '::/0' } it { expect(host.prefixlen).to be 0 } diff --git a/spec/unit/puppet_x/puppetlabs/firewall/utility_spec.rb b/spec/unit/puppet_x/puppetlabs/firewall/utility_spec.rb new file mode 100644 index 000000000..a62425d47 --- /dev/null +++ b/spec/unit/puppet_x/puppetlabs/firewall/utility_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'puppet_x' +require 'spec_helper' +require 'puppet/resource_api' +require 'puppet_x/puppetlabs/firewall/utility' + +RSpec.describe PuppetX::Firewall::Utility do # rubocop:disable RSpec/FilePath + let(:utility) { described_class } + + describe '#persist_iptables' do + before(:each) { Facter.clear } + + let(:context) { Puppet::ResourceApi::PuppetContext.new(Puppet::Type.type('firewall').type_definition.definition) } + + context 'when proto is IPv4' do + let(:proto) { 'IPv4' } + + it 'and OS family is RedHat' do + allow(Facter.fact('os')).to receive(:value).and_return({ 'family' => 'RedHat' }) + expect(Puppet::Provider).to receive(:execute).with(['/usr/libexec/iptables/iptables.init', 'save']) + + utility.persist_iptables(context, 'test', proto) + end + + it 'and OS family is Debian' do + allow(Facter.fact('os')).to receive(:value).and_return({ 'family' => 'Debian' }) + allow(Facter.fact(:iptables_persistent_version)).to receive(:value).and_return('0.4') + expect(Puppet::Provider).to receive(:execute).with(['/usr/sbin/service', 'iptables-persistent', 'save']) + + utility.persist_iptables(context, 'test', proto) + end + + it 'and OS family is Archlinux' do + allow(Facter.fact('os')).to receive(:value).and_return({ 'family' => 'Archlinux' }) + expect(Puppet::Provider).to receive(:execute).with(['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/iptables/iptables.rules']) + + utility.persist_iptables(context, 'test', proto) + end + + it 'and OS family is Suse' do + allow(Facter.fact('os')).to receive(:value).and_return({ 'family' => 'Suse' }) + expect(Puppet::Provider).to receive(:execute).with(['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/sysconfig/iptables']) + + utility.persist_iptables(context, 'test', proto) + end + end + + context 'when proto is IPv6' do + let(:proto) { 'IPv6' } + + it 'and OS family is RedHat' do + allow(Facter.fact('os')).to receive(:value).and_return({ 'family' => 'RedHat' }) + expect(Puppet::Provider).to receive(:execute).with(['/usr/libexec/iptables/ip6tables.init', 'save']) + + utility.persist_iptables(context, 'test', proto) + end + + it 'and OS family is Debian' do + allow(Facter.fact('os')).to receive(:value).and_return({ 'family' => 'Debian' }) + allow(Facter.fact(:iptables_persistent_version)).to receive(:value).and_return('1.2') + expect(Puppet::Provider).to receive(:execute).with(['/usr/sbin/service', 'netfilter-persistent', 'save']) + + utility.persist_iptables(context, 'test', proto) + end + + it 'and OS family is Archlinux' do + allow(Facter.fact('os')).to receive(:value).and_return({ 'family' => 'Archlinux' }) + expect(Puppet::Provider).to receive(:execute).with(['/bin/sh', '-c', '/usr/sbin/ip6tables-save > /etc/iptables/ip6tables.rules']) + + utility.persist_iptables(context, 'test', proto) + end + end + end + + describe '#create_absent' do + it { + expect(utility.create_absent(:name, { chain: 'INPUT', table: 'filter', protocol: 'IPv4' })).to eql({ chain: 'INPUT', table: 'filter', protocol: 'IPv4', ensure: 'absent' }) + } + + it { expect(utility.create_absent(:name, 'test')).to eql({ name: 'test', ensure: 'absent' }) } + end + + describe '#host_to_ip' do + it { + allow(Resolv).to receive(:each_address).at_least(:once).with('puppetlabs.com').and_yield('96.126.112.51').and_yield('2001:DB8:4650::13:8A') + expect(utility.host_to_ip('puppetlabs.com', 'IPv4')).to eql '96.126.112.51/32' + expect(utility.host_to_ip('puppetlabs.com', 'IPv6')).to eql '2001:db8:4650::13:8a/128' + } + + it { expect(utility.host_to_ip('96.126.112.51')).to eql '96.126.112.51/32' } + it { expect(utility.host_to_ip('96.126.112.51/32')).to eql '96.126.112.51/32' } + it { expect(utility.host_to_ip('2001:db8:85a3:0:0:8a2e:370:7334')).to eql '2001:db8:85a3::8a2e:370:7334/128' } + it { expect(utility.host_to_ip('2001:db8:1234::/48')).to eql '2001:db8:1234::/48' } + it { expect(utility.host_to_ip('0.0.0.0/0')).to be_nil } + it { expect(utility.host_to_ip('::/0')).to be_nil } + end + + describe '#host_to_mask' do + it { + allow(Resolv).to receive(:each_address).at_least(:once).with('puppetlabs.com').and_yield('96.126.112.51').and_yield('2001:DB8:4650::13:8A') + expect(utility.host_to_mask('puppetlabs.com', 'IPv4')).to eql '96.126.112.51/32' + expect(utility.host_to_mask('! puppetlabs.com', 'IPv6')).to eql '! 2001:db8:4650::13:8a/128' + } + + it { expect(utility.host_to_mask('96.126.112.51', 'IPv4')).to eql '96.126.112.51/32' } + it { expect(utility.host_to_mask('!96.126.112.51', 'IPv4')).to eql '! 96.126.112.51/32' } + it { expect(utility.host_to_mask('96.126.112.51/32', 'IPv4')).to eql '96.126.112.51/32' } + it { expect(utility.host_to_mask('! 96.126.112.51/32', 'IPv4')).to eql '! 96.126.112.51/32' } + it { expect(utility.host_to_mask('2001:db8:85a3:0:0:8a2e:370:7334', 'IPv6')).to eql '2001:db8:85a3::8a2e:370:7334/128' } + it { expect(utility.host_to_mask('!2001:db8:85a3:0:0:8a2e:370:7334', 'IPv6')).to eql '! 2001:db8:85a3::8a2e:370:7334/128' } + it { expect(utility.host_to_mask('2001:db8:1234::/48', 'IPv6')).to eql '2001:db8:1234::/48' } + it { expect(utility.host_to_mask('! 2001:db8:1234::/48', 'IPv6')).to eql '! 2001:db8:1234::/48' } + it { expect(utility.host_to_mask('0.0.0.0/0', 'IPv4')).to be_nil } + it { expect(utility.host_to_mask('!0.0.0.0/0', 'IPv4')).to be_nil } + it { expect(utility.host_to_mask('::/0', 'IPv6')).to be_nil } + it { expect(utility.host_to_mask('! ::/0', 'IPv6')).to be_nil } + end + + describe '#icmp_name_to_number' do + context 'with proto unsupported' do + ['inet5', 'inet8', 'foo'].each do |proto| + it "rejects invalid proto #{proto}" do + expect { utility.icmp_name_to_number('echo-reply', proto) } + .to raise_error(ArgumentError, "unsupported protocol family '#{proto}'") + end + end + end + + context 'with proto IPv4' do + let(:proto) { 'IPv4' } + + it { expect(utility.icmp_name_to_number('echo-reply', proto)).to eql '0' } + it { expect(utility.icmp_name_to_number('destination-unreachable', proto)).to eql '3' } + it { expect(utility.icmp_name_to_number('source-quench', proto)).to eql '4' } + it { expect(utility.icmp_name_to_number('redirect', proto)).to eql '6' } + it { expect(utility.icmp_name_to_number('echo-request', proto)).to eql '8' } + it { expect(utility.icmp_name_to_number('router-advertisement', proto)).to eql '9' } + it { expect(utility.icmp_name_to_number('router-solicitation', proto)).to eql '10' } + it { expect(utility.icmp_name_to_number('time-exceeded', proto)).to eql '11' } + it { expect(utility.icmp_name_to_number('parameter-problem', proto)).to eql '12' } + it { expect(utility.icmp_name_to_number('timestamp-request', proto)).to eql '13' } + it { expect(utility.icmp_name_to_number('timestamp-reply', proto)).to eql '14' } + it { expect(utility.icmp_name_to_number('address-mask-request', proto)).to eql '17' } + it { expect(utility.icmp_name_to_number('address-mask-reply', proto)).to eql '18' } + end + + context 'with proto IPv6' do + let(:proto) { 'IPv6' } + + it { expect(utility.icmp_name_to_number('destination-unreachable', proto)).to eql '1' } + it { expect(utility.icmp_name_to_number('time-exceeded', proto)).to eql '3' } + it { expect(utility.icmp_name_to_number('parameter-problem', proto)).to eql '4' } + it { expect(utility.icmp_name_to_number('echo-request', proto)).to eql '128' } + it { expect(utility.icmp_name_to_number('echo-reply', proto)).to eql '129' } + it { expect(utility.icmp_name_to_number('router-solicitation', proto)).to eql '133' } + it { expect(utility.icmp_name_to_number('router-advertisement', proto)).to eql '134' } + it { expect(utility.icmp_name_to_number('neighbour-solicitation', proto)).to eql '135' } + it { expect(utility.icmp_name_to_number('neighbour-advertisement', proto)).to eql '136' } + it { expect(utility.icmp_name_to_number('redirect', proto)).to eql '137' } + end + end + + describe '#log_level_name_to_number' do + it { expect(utility.log_level_name_to_number('2')).to eql '2' } + it { expect(utility.log_level_name_to_number('4')).to eql '4' } + it { expect(utility.log_level_name_to_number('panic')).to eql '0' } + it { expect(utility.log_level_name_to_number('alert')).to eql '1' } + it { expect(utility.log_level_name_to_number('crit')).to eql '2' } + it { expect(utility.log_level_name_to_number('err')).to eql '3' } + it { expect(utility.log_level_name_to_number('warn')).to eql '4' } + it { expect(utility.log_level_name_to_number('not')).to eql '5' } + it { expect(utility.log_level_name_to_number('info')).to eql '6' } + it { expect(utility.log_level_name_to_number('debug')).to eql '7' } + it { expect(utility.log_level_name_to_number('fail')).to be_nil } + end + + describe '#to_hex32' do + it { expect(utility.to_hex32('0')).to eql '0x0' } + it { expect(utility.to_hex32('0x32')).to eql '0x32' } + it { expect(utility.to_hex32('42')).to eql '0x2a' } + it { expect(utility.to_hex32('4294967295')).to eql '0xffffffff' } + it { expect(utility.to_hex32('4294967296')).to be_nil } + it { expect(utility.to_hex32('-1')).to be_nil } + it { expect(utility.to_hex32('bananas')).to be_nil } + end + + describe '#mark_mask_to_hex' do + it { expect(utility.mark_mask_to_hex('0')).to eql '0x0/0xffffffff' } + it { expect(utility.mark_mask_to_hex('0x32/0')).to eql '0x32/0x0' } + it { expect(utility.mark_mask_to_hex('42')).to eql '0x2a/0xffffffff' } + it { expect(utility.mark_mask_to_hex('4294967295/42')).to eql '0xffffffff/0x2a' } + end + + describe '#mark_to_hex' do + it { expect(utility.mark_to_hex('0')).to eql '0x0' } + it { expect(utility.mark_to_hex('! 0x32')).to eql '! 0x32' } + it { expect(utility.mark_to_hex('42')).to eql '0x2a' } + it { expect(utility.mark_to_hex('! 4294967295')).to eql '! 0xffffffff' } + end + + describe '#proto_number_to_name' do + it { expect(utility.proto_number_to_name('1')).to eql 'icmp' } + it { expect(utility.proto_number_to_name('2')).to eql 'igmp' } + it { expect(utility.proto_number_to_name('4')).to eql 'ipencap' } + it { expect(utility.proto_number_to_name('6')).to eql 'tcp' } + it { expect(utility.proto_number_to_name('7')).to eql 'cbt' } + it { expect(utility.proto_number_to_name('17')).to eql 'udp' } + it { expect(utility.proto_number_to_name('47')).to eql 'gre' } + it { expect(utility.proto_number_to_name('50')).to eql 'esp' } + it { expect(utility.proto_number_to_name('51')).to eql 'ah' } + it { expect(utility.proto_number_to_name('89')).to eql 'ospf' } + it { expect(utility.proto_number_to_name('103')).to eql 'pim' } + it { expect(utility.proto_number_to_name('112')).to eql 'vrrp' } + it { expect(utility.proto_number_to_name('132')).to eql 'sctp' } + + it 'rejects invalid number 619' do + expect { utility.proto_number_to_name('619') }.to raise_error(ArgumentError, 'Unsupported proto number: 619') + end + end + + describe '#dscp_number_to_class' do + it { expect(utility.dscp_number_to_class('0x0a')).to eql 'af11' } + it { expect(utility.dscp_number_to_class('0x0c')).to eql 'af12' } + it { expect(utility.dscp_number_to_class('0x0e')).to eql 'af13' } + it { expect(utility.dscp_number_to_class('0x12')).to eql 'af21' } + it { expect(utility.dscp_number_to_class('0x14')).to eql 'af22' } + it { expect(utility.dscp_number_to_class('0x16')).to eql 'af23' } + it { expect(utility.dscp_number_to_class('0x1a')).to eql 'af31' } + it { expect(utility.dscp_number_to_class('0x1c')).to eql 'af32' } + it { expect(utility.dscp_number_to_class('0x1e')).to eql 'af33' } + it { expect(utility.dscp_number_to_class('0x22')).to eql 'af41' } + it { expect(utility.dscp_number_to_class('0x24')).to eql 'af42' } + it { expect(utility.dscp_number_to_class('0x26')).to eql 'af43' } + it { expect(utility.dscp_number_to_class('0x08')).to eql 'cs1' } + it { expect(utility.dscp_number_to_class('0x10')).to eql 'cs2' } + it { expect(utility.dscp_number_to_class('0x18')).to eql 'cs3' } + it { expect(utility.dscp_number_to_class('0x20')).to eql 'cs4' } + it { expect(utility.dscp_number_to_class('0x28')).to eql 'cs5' } + it { expect(utility.dscp_number_to_class('0x30')).to eql 'cs6' } + it { expect(utility.dscp_number_to_class('0x38')).to eql 'cs7' } + it { expect(utility.dscp_number_to_class('0x2e')).to eql 'ef' } + it { expect(utility.dscp_number_to_class('0x66')).to be_nil } + end +end