From 5cab769134e51225ff084d6752be740f47463f23 Mon Sep 17 00:00:00 2001
From: Stephane de Labrusse
Date: Tue, 28 Jan 2025 15:55:59 +0100
Subject: [PATCH] feat(host set): add validation for IP range compatibility
with port forwards (#496)
NethServer/nethsecurity#1032
---
.../CreateOrEditPortForwardDrawer.vue | 29 ++++--
.../CreateOrEditHostSetDrawer.vue | 97 ++++++++++++++++++-
src/composables/useObjects.ts | 1 +
src/i18n/en/translation.json | 6 +-
4 files changed, 118 insertions(+), 15 deletions(-)
diff --git a/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue b/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue
index 67fa130c9..6760737cf 100644
--- a/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue
+++ b/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue
@@ -171,14 +171,17 @@ const restrictObjectsComboboxOptions = computed(() => {
label: t('standalone.port_forward.no_object')
}
- const restrictOptions = props.restrictObjectSuggestions.map((obj) => {
- return {
- id: obj.id,
- label: obj.name,
- description: t(`standalone.objects.subtype_${obj.subtype}`),
- icon: getObjectIcon(obj.subtype)
- }
- })
+ // filter out objects that contain other objects in their ipaddr
+ const restrictOptions = props.restrictObjectSuggestions
+ .filter((obj) => !obj.ipaddr?.some((ip: string) => ip.includes('objects/')))
+ .map((obj) => {
+ return {
+ id: obj.id,
+ label: obj.name,
+ description: t(`standalone.objects.subtype_${obj.subtype}`),
+ icon: getObjectIcon(obj.subtype)
+ }
+ })
return [noObjectOption, ...restrictOptions]
})
@@ -667,7 +670,15 @@ async function createOrEditPortForward() {
:selected-label="t('ne_combobox.selected')"
:user-input-label="t('ne_combobox.user_input_label')"
ref="destinationObjectRef"
- />
+ >
+
+ {{
+ t('standalone.port_forward.restricted_object_tooltip')
+ }}
+
+
import { useI18n } from 'vue-i18n'
+import { cloneDeep } from 'lodash-es'
import {
NeInlineNotification,
type NeComboboxOption,
@@ -20,6 +21,7 @@ import {
} from '@nethesis/vue-components'
import { ref, type PropType, watch, type Ref, computed } from 'vue'
import { ubusCall, ValidationError } from '@/lib/standalone/ubus'
+import type { AxiosResponse } from 'axios'
import {
MessageBag,
validateAlphanumeric,
@@ -43,8 +45,21 @@ const props = defineProps({
}
})
+type MatchInfo = {
+ database: string
+ family: 'ipv4' | 'ipv6'
+ id: string
+ name: string
+ type: string
+}
+
+type MatchInfoResponse = AxiosResponse<{
+ info: Record
+}>
+
const emit = defineEmits(['close', 'reloadData'])
+const portForwardsUsingHostSet = ref('')
const { t } = useI18n()
const name = ref('')
const nameRef = ref()
@@ -76,7 +91,13 @@ const ipVersionOptions = ref([
])
const recordOptionsButCurrent = computed(() => {
- return props.recordOptions?.filter((option) => option.id !== props.currentHostSet?.id)
+ // Filter out objects from recordOptions based on the presence of an IP address with a hyphen in allObjects
+ const objectsWithHyphenIp = props.allObjects
+ .filter((obj) => obj.ipaddr.some((ip: string) => ip.includes('-')))
+ .map((obj) => obj.id)
+ return props.recordOptions?.filter(
+ (option) => option.id !== props.currentHostSet?.id && !objectsWithHyphenIp.includes(option.id)
+ )
})
const allObjectsButCurrent = computed(() => {
@@ -94,7 +115,7 @@ watch(
// editing host or host set
name.value = props.currentHostSet.name
ipVersion.value = props.currentHostSet.family as IpVersion
- records.value = props.currentHostSet.ipaddr
+ records.value = cloneDeep(props.currentHostSet.ipaddr) // deep clone to avoid modifying the original array
} else {
// creating host or host set, reset form to defaults
name.value = ''
@@ -105,6 +126,18 @@ watch(
}
)
+// compute portForwardsUsingHostSet the name of the portforward rule using this object
+watch(
+ () => props.currentHostSet?.matches,
+ async (matches) => {
+ if (matches) {
+ portForwardsUsingHostSet.value = await getMatchedItemsName(matches)
+ } else {
+ portForwardsUsingHostSet.value = ''
+ }
+ }
+)
+
function closeDrawer() {
emit('close')
}
@@ -133,6 +166,50 @@ function runFieldValidators(
return validators.every((validator) => validator.valid)
}
+async function getMatchedItemsName(matches: string[]): Promise {
+ try {
+ const res: MatchInfoResponse = await ubusCall('ns.objects', 'get-info', { ids: matches })
+ const names: string[] = []
+ for (const match of Object.values(res.data.info)) {
+ if (match.type == 'redirect') {
+ names.push(match.name)
+ }
+ }
+ return names.join(', ')
+ } catch (error: any) {
+ console.error('Error fetching getMatchedItemsName:', error)
+ return ''
+ }
+}
+
+function validateNoIpRangeWithPortForward(records: Array) {
+ for (const record of records) {
+ if (record.includes('-') && portForwardsUsingHostSet.value) {
+ return {
+ valid: false,
+ errMessage: 'standalone.objects.range_not_compatible_with_port_forward'
+ }
+ }
+ }
+ return {
+ valid: true
+ }
+}
+
+function validateNoObjectsWithPortForward(records: Array) {
+ for (const record of records) {
+ if (record.includes('objects/') && portForwardsUsingHostSet.value) {
+ return {
+ valid: false,
+ errMessage: 'standalone.objects.objects_are_not_compatible_with_port_forward'
+ }
+ }
+ }
+ return {
+ valid: true
+ }
+}
+
function validateHostSetNotExists(value: string) {
if (allObjectsButCurrent.value?.find((obj) => obj.name === value && obj.subtype === 'host_set')) {
return {
@@ -158,7 +235,15 @@ function validate() {
nameRef
],
// records
- [[validateRequired(records.value[0])], 'ipaddr', recordRef]
+ [
+ [
+ validateNoObjectsWithPortForward(records.value),
+ validateNoIpRangeWithPortForward(records.value),
+ validateRequired(records.value[0])
+ ],
+ 'ipaddr',
+ recordRef
+ ]
]
// reset firstErrorRef for focus management
@@ -296,7 +381,11 @@ function deleteRecord(index: number) {
v-if="errorBag.getFirstI18nKeyFor('ipaddr')"
:class="'mt-2 text-sm text-rose-700 dark:text-rose-400'"
>
- {{ t(errorBag.getFirstI18nKeyFor('ipaddr')) }}
+ {{
+ t(errorBag.getFirstI18nKeyFor('ipaddr'), {
+ name: portForwardsUsingHostSet
+ })
+ }}
diff --git a/src/composables/useObjects.ts b/src/composables/useObjects.ts
index e9534aaf2..6d46a94ba 100644
--- a/src/composables/useObjects.ts
+++ b/src/composables/useObjects.ts
@@ -23,6 +23,7 @@ export type ObjectReference = {
family: IpVersion
used: boolean
matches: string[]
+ ipaddr?: string[]
}
/**
diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json
index b4e42954b..98f3e7760 100644
--- a/src/i18n/en/translation.json
+++ b/src/i18n/en/translation.json
@@ -1364,7 +1364,7 @@
"enter_restricted_addresses": "Enter restricted addresses",
"restricted_addresses": "Restricted addresses",
"restricted_object": "Restricted object",
- "restricted_object_tooltip": "All objects supported, except host sets with IP ranges",
+ "restricted_object_tooltip": "All objects supported, except host sets containing IP ranges or nested objects",
"no_object": "No object",
"port_forwards_for_destination_name": "Port forwards for destination '{name}'"
},
@@ -2276,7 +2276,9 @@
"host_set_already_exists": "Host set already exists",
"domain_set_already_exists": "Domain set already exists",
"delete_domain_set": "Delete domain set",
- "database_mwan3": "MultiWAN"
+ "database_mwan3": "MultiWAN",
+ "range_not_compatible_with_port_forward": "IP range is not compatible with port forwards. This host set is currently used by: {name}",
+ "objects_are_not_compatible_with_port_forward": "Objects are not compatible with port forwards. This host set is currently used by: {name}"
},
"ips": {
"title": "Intrusion Prevention System",