Skip to content

Commit 5cab769

Browse files
authored
feat(host set): add validation for IP range compatibility with port forwards (#496)
NethServer/nethsecurity#1032
1 parent 58bcbdd commit 5cab769

File tree

4 files changed

+118
-15
lines changed

4 files changed

+118
-15
lines changed

src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue

+20-9
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,17 @@ const restrictObjectsComboboxOptions = computed(() => {
171171
label: t('standalone.port_forward.no_object')
172172
}
173173

174-
const restrictOptions = props.restrictObjectSuggestions.map((obj) => {
175-
return {
176-
id: obj.id,
177-
label: obj.name,
178-
description: t(`standalone.objects.subtype_${obj.subtype}`),
179-
icon: getObjectIcon(obj.subtype)
180-
}
181-
})
174+
// filter out objects that contain other objects in their ipaddr
175+
const restrictOptions = props.restrictObjectSuggestions
176+
.filter((obj) => !obj.ipaddr?.some((ip: string) => ip.includes('objects/')))
177+
.map((obj) => {
178+
return {
179+
id: obj.id,
180+
label: obj.name,
181+
description: t(`standalone.objects.subtype_${obj.subtype}`),
182+
icon: getObjectIcon(obj.subtype)
183+
}
184+
})
182185

183186
return [noObjectOption, ...restrictOptions]
184187
})
@@ -667,7 +670,15 @@ async function createOrEditPortForward() {
667670
:selected-label="t('ne_combobox.selected')"
668671
:user-input-label="t('ne_combobox.user_input_label')"
669672
ref="destinationObjectRef"
670-
/>
673+
>
674+
<template #tooltip>
675+
<NeTooltip
676+
><template #content>{{
677+
t('standalone.port_forward.restricted_object_tooltip')
678+
}}</template></NeTooltip
679+
>
680+
</template>
681+
</NeCombobox>
671682
<NeTextInput
672683
v-if="!anyProtocolSelected"
673684
:label="t('standalone.port_forward.destination_port')"

src/components/standalone/users_objects/CreateOrEditHostSetDrawer.vue

+93-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
<script setup lang="ts">
77
import { useI18n } from 'vue-i18n'
8+
import { cloneDeep } from 'lodash-es'
89
import {
910
NeInlineNotification,
1011
type NeComboboxOption,
@@ -20,6 +21,7 @@ import {
2021
} from '@nethesis/vue-components'
2122
import { ref, type PropType, watch, type Ref, computed } from 'vue'
2223
import { ubusCall, ValidationError } from '@/lib/standalone/ubus'
24+
import type { AxiosResponse } from 'axios'
2325
import {
2426
MessageBag,
2527
validateAlphanumeric,
@@ -43,8 +45,21 @@ const props = defineProps({
4345
}
4446
})
4547

48+
type MatchInfo = {
49+
database: string
50+
family: 'ipv4' | 'ipv6'
51+
id: string
52+
name: string
53+
type: string
54+
}
55+
56+
type MatchInfoResponse = AxiosResponse<{
57+
info: Record<string, MatchInfo>
58+
}>
59+
4660
const emit = defineEmits(['close', 'reloadData'])
4761

62+
const portForwardsUsingHostSet = ref('')
4863
const { t } = useI18n()
4964
const name = ref('')
5065
const nameRef = ref()
@@ -76,7 +91,13 @@ const ipVersionOptions = ref([
7691
])
7792

7893
const recordOptionsButCurrent = computed(() => {
79-
return props.recordOptions?.filter((option) => option.id !== props.currentHostSet?.id)
94+
// Filter out objects from recordOptions based on the presence of an IP address with a hyphen in allObjects
95+
const objectsWithHyphenIp = props.allObjects
96+
.filter((obj) => obj.ipaddr.some((ip: string) => ip.includes('-')))
97+
.map((obj) => obj.id)
98+
return props.recordOptions?.filter(
99+
(option) => option.id !== props.currentHostSet?.id && !objectsWithHyphenIp.includes(option.id)
100+
)
80101
})
81102

82103
const allObjectsButCurrent = computed(() => {
@@ -94,7 +115,7 @@ watch(
94115
// editing host or host set
95116
name.value = props.currentHostSet.name
96117
ipVersion.value = props.currentHostSet.family as IpVersion
97-
records.value = props.currentHostSet.ipaddr
118+
records.value = cloneDeep(props.currentHostSet.ipaddr) // deep clone to avoid modifying the original array
98119
} else {
99120
// creating host or host set, reset form to defaults
100121
name.value = ''
@@ -105,6 +126,18 @@ watch(
105126
}
106127
)
107128

129+
// compute portForwardsUsingHostSet the name of the portforward rule using this object
130+
watch(
131+
() => props.currentHostSet?.matches,
132+
async (matches) => {
133+
if (matches) {
134+
portForwardsUsingHostSet.value = await getMatchedItemsName(matches)
135+
} else {
136+
portForwardsUsingHostSet.value = ''
137+
}
138+
}
139+
)
140+
108141
function closeDrawer() {
109142
emit('close')
110143
}
@@ -133,6 +166,50 @@ function runFieldValidators(
133166
return validators.every((validator) => validator.valid)
134167
}
135168

169+
async function getMatchedItemsName(matches: string[]): Promise<string> {
170+
try {
171+
const res: MatchInfoResponse = await ubusCall('ns.objects', 'get-info', { ids: matches })
172+
const names: string[] = []
173+
for (const match of Object.values(res.data.info)) {
174+
if (match.type == 'redirect') {
175+
names.push(match.name)
176+
}
177+
}
178+
return names.join(', ')
179+
} catch (error: any) {
180+
console.error('Error fetching getMatchedItemsName:', error)
181+
return ''
182+
}
183+
}
184+
185+
function validateNoIpRangeWithPortForward(records: Array<string>) {
186+
for (const record of records) {
187+
if (record.includes('-') && portForwardsUsingHostSet.value) {
188+
return {
189+
valid: false,
190+
errMessage: 'standalone.objects.range_not_compatible_with_port_forward'
191+
}
192+
}
193+
}
194+
return {
195+
valid: true
196+
}
197+
}
198+
199+
function validateNoObjectsWithPortForward(records: Array<string>) {
200+
for (const record of records) {
201+
if (record.includes('objects/') && portForwardsUsingHostSet.value) {
202+
return {
203+
valid: false,
204+
errMessage: 'standalone.objects.objects_are_not_compatible_with_port_forward'
205+
}
206+
}
207+
}
208+
return {
209+
valid: true
210+
}
211+
}
212+
136213
function validateHostSetNotExists(value: string) {
137214
if (allObjectsButCurrent.value?.find((obj) => obj.name === value && obj.subtype === 'host_set')) {
138215
return {
@@ -158,7 +235,15 @@ function validate() {
158235
nameRef
159236
],
160237
// records
161-
[[validateRequired(records.value[0])], 'ipaddr', recordRef]
238+
[
239+
[
240+
validateNoObjectsWithPortForward(records.value),
241+
validateNoIpRangeWithPortForward(records.value),
242+
validateRequired(records.value[0])
243+
],
244+
'ipaddr',
245+
recordRef
246+
]
162247
]
163248

164249
// reset firstErrorRef for focus management
@@ -296,7 +381,11 @@ function deleteRecord(index: number) {
296381
v-if="errorBag.getFirstI18nKeyFor('ipaddr')"
297382
:class="'mt-2 text-sm text-rose-700 dark:text-rose-400'"
298383
>
299-
{{ t(errorBag.getFirstI18nKeyFor('ipaddr')) }}
384+
{{
385+
t(errorBag.getFirstI18nKeyFor('ipaddr'), {
386+
name: portForwardsUsingHostSet
387+
})
388+
}}
300389
</p>
301390
<NeButton class="mt-4" size="md" @click="addRecord" kind="secondary">
302391
<template #prefix>

src/composables/useObjects.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type ObjectReference = {
2323
family: IpVersion
2424
used: boolean
2525
matches: string[]
26+
ipaddr?: string[]
2627
}
2728

2829
/**

src/i18n/en/translation.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1364,7 +1364,7 @@
13641364
"enter_restricted_addresses": "Enter restricted addresses",
13651365
"restricted_addresses": "Restricted addresses",
13661366
"restricted_object": "Restricted object",
1367-
"restricted_object_tooltip": "All objects supported, except host sets with IP ranges",
1367+
"restricted_object_tooltip": "All objects supported, except host sets containing IP ranges or nested objects",
13681368
"no_object": "No object",
13691369
"port_forwards_for_destination_name": "Port forwards for destination '{name}'"
13701370
},
@@ -2276,7 +2276,9 @@
22762276
"host_set_already_exists": "Host set already exists",
22772277
"domain_set_already_exists": "Domain set already exists",
22782278
"delete_domain_set": "Delete domain set",
2279-
"database_mwan3": "MultiWAN"
2279+
"database_mwan3": "MultiWAN",
2280+
"range_not_compatible_with_port_forward": "IP range is not compatible with port forwards. This host set is currently used by: {name}",
2281+
"objects_are_not_compatible_with_port_forward": "Objects are not compatible with port forwards. This host set is currently used by: {name}"
22802282
},
22812283
"ips": {
22822284
"title": "Intrusion Prevention System",

0 commit comments

Comments
 (0)