diff --git a/.changes/issue-458.md b/.changes/issue-458.md index 0f78f7b5..06c2524f 100644 --- a/.changes/issue-458.md +++ b/.changes/issue-458.md @@ -3,3 +3,4 @@ ENHANCEMENTS: * **resource/junos_security**: * resource now use new [terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework) and some of config errors are now sent during Plan instead of during Apply (optional boolean attributes doesn't accept value *false*, optional string attributes doesn't accept *empty* value, the resource schema has been upgraded to have one-blocks in single mode instead of list) + * add `nat_source` block argument (Fix [#458](https://github.com/jeremmfr/terraform-provider-junos/issues/458)) diff --git a/docs/resources/security.md b/docs/resources/security.md index ea6f638d..e8be1dae 100644 --- a/docs/resources/security.md +++ b/docs/resources/security.md @@ -63,6 +63,39 @@ The following arguments are supported: - **log** (Optional, Block) Declare `log` configuration. See [below for nested schema](#log-arguments). +- **nat_source** (Optional, Block) + Declare `nat source` configuration. + - **address_persistent** (Optional, Boolean) + Allow source address to maintain same translation. + - **interface_port_overloading_factor** (Optional, Number) + Port overloading factor for interface NAT. + Conflict with `interface_port_overloading_off`. + - **interface_port_overloading_off** (Optional, Boolean) + Turn off interface port over-loading. + Conflict with `interface_port_overloading_factor`. + - **pool_default_port_range** (Optional, Number) + Configure Source NAT default port range lower limit. + `pool_default_port_range_to` must also be specified. + - **pool_default_port_range_to** (Optional, Number) + Configure Source NAT default port range upper limit. + `pool_default_port_range` must also be specified. + - **pool_default_twin_port_range** (Optional, Number) + Configure Source NAT default twin port range lower limit. + `pool_default_twin_port_range_to` must also be specified. + - **pool_default_twin_port_range_to** (Optional, Number) + Configure Source NAT default twin port range upper limit. + `pool_default_twin_port_range` must also be specified. + - **pool_utilization_alarm_clear_threshold** (Optional, Number) + Clear threshold for pool utilization alarm (40..100). + `pool_utilization_alarm_raise_threshold` must also be specified. + - **pool_utilization_alarm_raise_threshold** (Optional, Number) + Raise threshold for pool utilization alarm (50..100). + - **port_randomization_disable** (Optional, Boolean) + Disable Source NAT port randomization. + - **session_drop_hold_down** (Optional, Number) + Session drop hold down time (30..28800). + - **session_persistence_scan** (Optional, Boolean) + Allow source to maintain session when session scan. - **policies** (Optional, Block) Declare `policies` configuration. - **policy_rematch** (Optional, Boolean) diff --git a/internal/providerfwk/resource_security.go b/internal/providerfwk/resource_security.go index 2b927afb..d5da5f57 100644 --- a/internal/providerfwk/resource_security.go +++ b/internal/providerfwk/resource_security.go @@ -1040,6 +1040,95 @@ func (rsc *security) Schema( tfplanmodifier.BlockRemoveNull(), }, }, + "nat_source": schema.SingleNestedBlock{ + Description: "Declare `nat source` configuration.", + Attributes: map[string]schema.Attribute{ + "address_persistent": schema.BoolAttribute{ + Optional: true, + Description: "Allow source address to maintain same translation.", + Validators: []validator.Bool{ + tfvalidator.BoolTrue(), + }, + }, + "interface_port_overloading_factor": schema.Int64Attribute{ + Optional: true, + Description: "Port overloading factor for interface NAT.", + Validators: []validator.Int64{ + int64validator.Between(0, 65535), + }, + }, + "interface_port_overloading_off": schema.BoolAttribute{ + Optional: true, + Description: "Turn off interface port over-loading.", + Validators: []validator.Bool{ + tfvalidator.BoolTrue(), + }, + }, + "pool_default_port_range": schema.Int64Attribute{ + Optional: true, + Description: "Configure Source NAT default port range lower limit.", + Validators: []validator.Int64{ + int64validator.Between(1024, 63487), + }, + }, + "pool_default_port_range_to": schema.Int64Attribute{ + Optional: true, + Description: "Configure Source NAT default port range upper limit.", + Validators: []validator.Int64{ + int64validator.Between(1024, 63487), + }, + }, + "pool_default_twin_port_range": schema.Int64Attribute{ + Optional: true, + Description: "Configure Source NAT default twin port range lower limit.", + Validators: []validator.Int64{ + int64validator.Between(63488, 65535), + }, + }, + "pool_default_twin_port_range_to": schema.Int64Attribute{ + Optional: true, + Description: "Configure Source NAT default twin port range upper limit.", + Validators: []validator.Int64{ + int64validator.Between(63488, 65535), + }, + }, + "pool_utilization_alarm_clear_threshold": schema.Int64Attribute{ + Optional: true, + Description: "Clear threshold for pool utilization alarm (40..100).", + Validators: []validator.Int64{ + int64validator.Between(40, 100), + }, + }, + "pool_utilization_alarm_raise_threshold": schema.Int64Attribute{ + Optional: true, + Description: "Raise threshold for pool utilization alarm (50..100).", + Validators: []validator.Int64{ + int64validator.Between(50, 100), + }, + }, + "port_randomization_disable": schema.BoolAttribute{ + Optional: true, + Description: "Disable Source NAT port randomization.", + Validators: []validator.Bool{ + tfvalidator.BoolTrue(), + }, + }, + "session_drop_hold_down": schema.Int64Attribute{ + Optional: true, + Description: "Session drop hold down time (30..28800).", + Validators: []validator.Int64{ + int64validator.Between(30, 28800), + }, + }, + "session_persistence_scan": schema.BoolAttribute{ + Optional: true, + Description: "Allow source to maintain session when session scan.", + Validators: []validator.Bool{ + tfvalidator.BoolTrue(), + }, + }, + }, + }, "policies": schema.SingleNestedBlock{ Description: "Declare `policies` configuration.", Attributes: map[string]schema.Attribute{ @@ -1182,6 +1271,7 @@ type securityData struct { IdpSensorConfiguration *securityBlockIdpSensorConfiguration `tfsdk:"idp_sensor_configuration"` IkeTraceoptions *securityBlockIkeTraceoptions `tfsdk:"ike_traceoptions"` Log *securityBlockLog `tfsdk:"log"` + NatSource *securityBlockNatSource `tfsdk:"nat_source"` Policies *securityBlockPolicies `tfsdk:"policies"` UserIdentificationAuthSource *securityBlockUserIdentificationAuthSource `tfsdk:"user_identification_auth_source"` Utm *securityBlockUtm `tfsdk:"utm"` @@ -1198,6 +1288,7 @@ type securityConfig struct { IdpSensorConfiguration *securityBlockIdpSensorConfiguration `tfsdk:"idp_sensor_configuration"` IkeTraceoptions *securityBlockIkeTraceoptionsConfig `tfsdk:"ike_traceoptions"` Log *securityBlockLog `tfsdk:"log"` + NatSource *securityBlockNatSource `tfsdk:"nat_source"` Policies *securityBlockPolicies `tfsdk:"policies"` UserIdentificationAuthSource *securityBlockUserIdentificationAuthSource `tfsdk:"user_identification_auth_source"` Utm *securityBlockUtm `tfsdk:"utm"` @@ -1704,6 +1795,52 @@ type securityBlockLogBlockTransport struct { TLSProfile types.String `tfsdk:"tls_profile"` } +type securityBlockNatSource struct { + AddressPersistent types.Bool `tfsdk:"address_persistent"` + InterfacePortOverloadingOff types.Bool `tfsdk:"interface_port_overloading_off"` + PortRandomizationDisable types.Bool `tfsdk:"port_randomization_disable"` + SessionPersistenceScan types.Bool `tfsdk:"session_persistence_scan"` + InterfacePortOverloadingFactor types.Int64 `tfsdk:"interface_port_overloading_factor"` + PoolDefaultPortRange types.Int64 `tfsdk:"pool_default_port_range"` + PoolDefaultPortRangeTo types.Int64 `tfsdk:"pool_default_port_range_to"` + PoolDefaultTwinPortRange types.Int64 `tfsdk:"pool_default_twin_port_range"` + PoolDefaultTwinPortRangeTo types.Int64 `tfsdk:"pool_default_twin_port_range_to"` + PoolUtilizationAlarmClearThreshold types.Int64 `tfsdk:"pool_utilization_alarm_clear_threshold"` + PoolUtilizationAlarmRaiseThreshold types.Int64 `tfsdk:"pool_utilization_alarm_raise_threshold"` + SessionDropHoldDown types.Int64 `tfsdk:"session_drop_hold_down"` +} + +func (block *securityBlockNatSource) isEmpty() bool { + switch { + case !block.AddressPersistent.IsNull(): + return false + case !block.InterfacePortOverloadingOff.IsNull(): + return false + case !block.PortRandomizationDisable.IsNull(): + return false + case !block.SessionPersistenceScan.IsNull(): + return false + case !block.InterfacePortOverloadingFactor.IsNull(): + return false + case !block.PoolDefaultPortRange.IsNull(): + return false + case !block.PoolDefaultPortRangeTo.IsNull(): + return false + case !block.PoolDefaultTwinPortRange.IsNull(): + return false + case !block.PoolDefaultTwinPortRangeTo.IsNull(): + return false + case !block.PoolUtilizationAlarmClearThreshold.IsNull(): + return false + case !block.PoolUtilizationAlarmRaiseThreshold.IsNull(): + return false + case !block.SessionDropHoldDown.IsNull(): + return false + default: + return true + } +} + type securityBlockPolicies struct { PolicyRematch types.Bool `tfsdk:"policy_rematch"` PolicyRematchExtensive types.Bool `tfsdk:"policy_rematch_extensive"` @@ -2005,6 +2142,80 @@ func (rsc *security) ValidateConfig( } } + if config.NatSource != nil { + if config.NatSource.isEmpty() { + resp.Diagnostics.AddAttributeError( + path.Root("nat_source").AtName("*"), + "Missing Configuration Error", + "nat_source block is empty", + ) + } + if !config.NatSource.InterfacePortOverloadingFactor.IsNull() && + !config.NatSource.InterfacePortOverloadingOff.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("nat_source").AtName("interface_port_overloading_off"), + "Conflict Configuration Error", + "interface_port_overloading_off and interface_port_overloading_factor cannot be configured together "+ + "in nat_source block", + ) + } + if !config.NatSource.PoolDefaultPortRangeTo.IsNull() && + config.NatSource.PoolDefaultPortRange.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("nat_source").AtName("pool_default_port_range_to"), + "Missing Configuration Error", + "pool_default_port_range must be specified with pool_default_port_range_to in nat_source block", + ) + } + if !config.NatSource.PoolDefaultPortRange.IsNull() && + config.NatSource.PoolDefaultPortRangeTo.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("nat_source").AtName("pool_default_port_range"), + "Missing Configuration Error", + "pool_default_port_range_to must be specified with pool_default_port_range in nat_source block", + ) + } + if !config.NatSource.PoolDefaultTwinPortRangeTo.IsNull() && + config.NatSource.PoolDefaultTwinPortRange.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("nat_source").AtName("pool_default_twin_port_range_to"), + "Missing Configuration Error", + "pool_default_twin_port_range must be specified with pool_default_twin_port_range_to in nat_source block", + ) + } + if !config.NatSource.PoolDefaultTwinPortRange.IsNull() && + config.NatSource.PoolDefaultTwinPortRangeTo.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("nat_source").AtName("pool_default_twin_port_range"), + "Missing Configuration Error", + "pool_default_twin_port_range_to must be specified with pool_default_twin_port_range in nat_source block", + ) + } + if !config.NatSource.PoolUtilizationAlarmClearThreshold.IsNull() && + config.NatSource.PoolUtilizationAlarmRaiseThreshold.IsNull() { + resp.Diagnostics.AddAttributeError( + path.Root("nat_source").AtName("pool_utilization_alarm_clear_threshold"), + "Missing Configuration Error", + "pool_utilization_alarm_raise_threshold must be specified with pool_utilization_alarm_clear_threshold "+ + "in nat_source block", + ) + } + if !config.NatSource.PoolUtilizationAlarmClearThreshold.IsNull() && + !config.NatSource.PoolUtilizationAlarmClearThreshold.IsUnknown() && + !config.NatSource.PoolUtilizationAlarmRaiseThreshold.IsNull() && + !config.NatSource.PoolUtilizationAlarmRaiseThreshold.IsUnknown() { + if config.NatSource.PoolUtilizationAlarmClearThreshold.ValueInt64() > + config.NatSource.PoolUtilizationAlarmRaiseThreshold.ValueInt64() { + resp.Diagnostics.AddAttributeError( + path.Root("nat_source").AtName("pool_utilization_alarm_clear_threshold"), + "Conflict Configuration Error", + "pool_utilization_alarm_clear_threshold must be larger than "+ + "pool_utilization_alarm_raise_threshold in nat_source block", + ) + } + } + } + if config.Policies != nil { if config.Policies.isEmpty() { resp.Diagnostics.AddAttributeError( @@ -2379,6 +2590,14 @@ func (rscData *securityData) set( } configSet = append(configSet, blockSet...) } + if rscData.NatSource != nil { + if rscData.NatSource.isEmpty() { + return path.Root("nat_source").AtName("*"), + fmt.Errorf("nat_source block is empty") + } + + configSet = append(configSet, rscData.NatSource.configSet()...) + } if rscData.Policies != nil { if rscData.Policies.isEmpty() { return path.Root("policies").AtName("*"), @@ -2892,6 +3111,58 @@ func (block *securityBlockLog) configSet() ( return configSet, path.Empty(), nil } +func (block *securityBlockNatSource) configSet() []string { + setPrefix := "set security nat source " + configSet := make([]string, 0) + + if block.AddressPersistent.ValueBool() { + configSet = append(configSet, setPrefix+"address-persistent") + } + if !block.InterfacePortOverloadingFactor.IsNull() { + configSet = append(configSet, setPrefix+"interface port-overloading-factor "+ + utils.ConvI64toa(block.InterfacePortOverloadingFactor.ValueInt64())) + } + if block.InterfacePortOverloadingOff.ValueBool() { + configSet = append(configSet, setPrefix+"interface port-overloading off") + } + if !block.PoolDefaultPortRange.IsNull() { + configSet = append(configSet, setPrefix+"pool-default-port-range "+ + utils.ConvI64toa(block.PoolDefaultPortRange.ValueInt64())) + } + if !block.PoolDefaultPortRangeTo.IsNull() { + configSet = append(configSet, setPrefix+"pool-default-port-range to "+ + utils.ConvI64toa(block.PoolDefaultPortRangeTo.ValueInt64())) + } + if !block.PoolDefaultTwinPortRange.IsNull() { + configSet = append(configSet, setPrefix+"pool-default-twin-port-range "+ + utils.ConvI64toa(block.PoolDefaultTwinPortRange.ValueInt64())) + } + if !block.PoolDefaultTwinPortRangeTo.IsNull() { + configSet = append(configSet, setPrefix+"pool-default-twin-port-range to "+ + utils.ConvI64toa(block.PoolDefaultTwinPortRangeTo.ValueInt64())) + } + if !block.PoolUtilizationAlarmClearThreshold.IsNull() { + configSet = append(configSet, setPrefix+"pool-utilization-alarm clear-threshold "+ + utils.ConvI64toa(block.PoolUtilizationAlarmClearThreshold.ValueInt64())) + } + if !block.PoolUtilizationAlarmRaiseThreshold.IsNull() { + configSet = append(configSet, setPrefix+"pool-utilization-alarm raise-threshold "+ + utils.ConvI64toa(block.PoolUtilizationAlarmRaiseThreshold.ValueInt64())) + } + if block.PortRandomizationDisable.ValueBool() { + configSet = append(configSet, setPrefix+"port-randomization disable") + } + if !block.SessionDropHoldDown.IsNull() { + configSet = append(configSet, setPrefix+"session-drop-hold-down "+ + utils.ConvI64toa(block.SessionDropHoldDown.ValueInt64())) + } + if block.SessionPersistenceScan.ValueBool() { + configSet = append(configSet, setPrefix+"session-persistence-scan") + } + + return configSet +} + func (block *securityBlockUserIdentificationAuthSource) configSet() []string { setPrefix := "set security user-identification authentication-source " configSet := make([]string, 0) @@ -3021,6 +3292,13 @@ func (rscData *securityData) read( if err := rscData.IkeTraceoptions.read(itemTrim); err != nil { return err } + case bchk.StringHasOneOfPrefixes(itemTrim, securityBlockNatSource{}.lines()): + if rscData.NatSource == nil { + rscData.NatSource = &securityBlockNatSource{} + } + if err := rscData.NatSource.read(itemTrim); err != nil { + return err + } case bchk.StringHasOneOfPrefixes(itemTrim, securityBlockPolicies{}.lines()): if rscData.Policies == nil { rscData.Policies = &securityBlockPolicies{} @@ -3517,6 +3795,42 @@ func (block *securityBlockLog) read(itemTrim string) (err error) { return nil } +func (block *securityBlockNatSource) read(itemTrim string) (err error) { + balt.CutPrefixInString(&itemTrim, "nat source ") + + switch { + case itemTrim == "address-persistent": + block.AddressPersistent = types.BoolValue(true) + case balt.CutPrefixInString(&itemTrim, "interface port-overloading-factor "): + block.InterfacePortOverloadingFactor, err = tfdata.ConvAtoi64Value(itemTrim) + case itemTrim == "interface port-overloading off": + block.InterfacePortOverloadingOff = types.BoolValue(true) + case balt.CutPrefixInString(&itemTrim, "pool-default-port-range to "): + block.PoolDefaultPortRangeTo, err = tfdata.ConvAtoi64Value(itemTrim) + case balt.CutPrefixInString(&itemTrim, "pool-default-port-range "): + block.PoolDefaultPortRange, err = tfdata.ConvAtoi64Value(itemTrim) + case balt.CutPrefixInString(&itemTrim, "pool-default-twin-port-range to "): + block.PoolDefaultTwinPortRangeTo, err = tfdata.ConvAtoi64Value(itemTrim) + case balt.CutPrefixInString(&itemTrim, "pool-default-twin-port-range "): + block.PoolDefaultTwinPortRange, err = tfdata.ConvAtoi64Value(itemTrim) + case balt.CutPrefixInString(&itemTrim, "pool-utilization-alarm clear-threshold "): + block.PoolUtilizationAlarmClearThreshold, err = tfdata.ConvAtoi64Value(itemTrim) + case balt.CutPrefixInString(&itemTrim, "pool-utilization-alarm raise-threshold "): + block.PoolUtilizationAlarmRaiseThreshold, err = tfdata.ConvAtoi64Value(itemTrim) + case itemTrim == "port-randomization disable": + block.PortRandomizationDisable = types.BoolValue(true) + case balt.CutPrefixInString(&itemTrim, "session-drop-hold-down "): + block.SessionDropHoldDown, err = tfdata.ConvAtoi64Value(itemTrim) + case itemTrim == "session-persistence-scan": + block.SessionPersistenceScan = types.BoolValue(true) + } + if err != nil { + return err + } + + return nil +} + func (block *securityBlockUserIdentificationAuthSource) read(itemTrim string) (err error) { balt.CutPrefixInString(&itemTrim, "user-identification authentication-source ") @@ -3580,6 +3894,7 @@ func (rscData securityData) del( listLinesToDelete = append(listLinesToDelete, securityBlockIdpSensorConfiguration{}.lines()...) listLinesToDelete = append(listLinesToDelete, securityBlockIkeTraceoptions{}.lines()...) listLinesToDelete = append(listLinesToDelete, securityBlockLog{}.lines()...) + listLinesToDelete = append(listLinesToDelete, securityBlockNatSource{}.lines()...) listLinesToDelete = append(listLinesToDelete, securityBlockPolicies{}.lines()...) listLinesToDelete = append(listLinesToDelete, securityBlockUserIdentificationAuthSource{}.lines()...) listLinesToDelete = append(listLinesToDelete, securityBlockUtm{}.lines()...) @@ -3690,6 +4005,20 @@ func (block securityBlockLog) lines() []string { } } +func (block securityBlockNatSource) lines() []string { + return []string{ + "nat source address-persistent", + "nat source interface port-overloading", + "nat source interface port-overloading-factor", + "nat source pool-default-port-range", + "nat source pool-default-twin-port-range", + "nat source pool-utilization-alarm", + "nat source port-randomization", + "nat source session-drop-hold-down", + "nat source session-persistence-scan", + } +} + func (block securityBlockPolicies) lines() []string { return []string{ "policies policy-rematch", diff --git a/internal/providerfwk/resource_security_test.go b/internal/providerfwk/resource_security_test.go index b3eb53c7..74eec1f0 100644 --- a/internal/providerfwk/resource_security_test.go +++ b/internal/providerfwk/resource_security_test.go @@ -361,6 +361,10 @@ resource "junos_security" "testacc_security" { } utc_timestamp = true } + nat_source { + interface_port_overloading_off = true + pool_utilization_alarm_raise_threshold = 90 + } policies { policy_rematch = true } @@ -438,6 +442,19 @@ resource "junos_security" "testacc_security" { rate_cap = 100 source_address = "192.0.2.1" } + nat_source { + address_persistent = true + interface_port_overloading_factor = 32 + pool_default_port_range = 10242 + pool_default_port_range_to = 20242 + pool_default_twin_port_range = 64000 + pool_default_twin_port_range_to = 65001 + pool_utilization_alarm_clear_threshold = 45 + pool_utilization_alarm_raise_threshold = 80 + port_randomization_disable = true + session_drop_hold_down = 600 + session_persistence_scan = true + } policies { policy_rematch_extensive = true }