diff --git a/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue b/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue index e98ed8f85..8d91c7031 100644 --- a/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue +++ b/src/components/standalone/firewall/CreateOrEditPortForwardDrawer.vue @@ -99,6 +99,18 @@ const supportedDestinationZones = computed(() => { } }) +const trafficType = ref<'select_protocols' | 'all_traffic'>('select_protocols') +const trafficTypeOptions = [ + { + id: 'select_protocols', + label: t('standalone.port_forward.select_protocols') + }, + { + id: 'all_traffic', + label: t('standalone.port_forward.all_traffic') + } +] + // Form fields const id = ref('') const name = ref('') @@ -125,7 +137,7 @@ const reflectionZones = ref([]) const reflectionZonesRef = ref() const destinationZone = ref('') const destinationZoneRef = ref() -const destinationAddressType = ref<'address' | 'object'>('address') +const destinationAddressType = ref<'address' | 'object' | 'firewall'>('address') const destinationAddressObject = ref('') const destinationObjectRef = ref() const restrictType = ref<'address' | 'object'>('address') @@ -140,6 +152,10 @@ const destinationAddressOptions = ref([ { id: 'object', label: t('standalone.port_forward.select_an_object') + }, + { + id: 'firewall', + label: t('standalone.port_forward.firewall') } ]) @@ -186,7 +202,13 @@ const restrictObjectsComboboxOptions = computed(() => { return [noObjectOption, ...restrictOptions] }) -const anyProtocolSelected = computed(() => protocols.value.length == 0) +const filteredDestinationAddressOptions = computed(() => { + // when traffic type is 'all_traffic', exclude the firewall option + if (trafficType.value === 'all_traffic') { + return destinationAddressOptions.value.filter((option) => option.id !== 'firewall') + } + return destinationAddressOptions.value +}) function resetForm() { validationErrorBag.value.clear() @@ -203,11 +225,16 @@ function resetForm() { destinationAddressType.value = 'object' destinationIP.value = '' destinationAddressObject.value = props.initialItem.ns_dst - } else { - // destination address is an IP address + } else if (props.initialItem.dest_ip && props.initialItem.dest_ip !== '127.0.0.1') { + // destination address is a custom IP address destinationAddressType.value = 'address' destinationIP.value = props.initialItem.dest_ip destinationAddressObject.value = '' + } else { + // destination address is 127.0.0.1 (firewall's IP) + destinationAddressType.value = 'firewall' + destinationIP.value = '' + destinationAddressObject.value = '' } } else { // creating new port forward @@ -266,12 +293,13 @@ async function fetchOptions() { loading.value.listProtocolsAndZones = true try { - supportedProtocols.value = ( - await ubusCall('ns.redirects', 'list-protocols') - ).data.protocols.map((proto: string) => ({ - id: proto, - label: proto.toUpperCase() - })) + supportedProtocols.value = (await ubusCall('ns.redirects', 'list-protocols')).data.protocols + // filter out 'all' protocol as there is the dedicated 'all_traffic' option in the UI + .filter((proto: string) => proto.toLowerCase() !== 'all') + .map((proto: string) => ({ + id: proto, + label: proto.toUpperCase() + })) } catch (err: any) { error.value.notificationTitle = t('error.cannot_retrieve_protocols') error.value.notificationDescription = t(getAxiosErrorMessage(err)) @@ -318,6 +346,16 @@ watchEffect(() => { resetForm() }) +// reset destination address type to 'address' when switching to all_traffic and firewall is selected +watch( + () => trafficType.value, + (newTrafficType) => { + if (newTrafficType === 'all_traffic' && destinationAddressType.value === 'firewall') { + destinationAddressType.value = 'address' + } + } +) + function close() { error.value.notificationTitle = '' error.value.notificationDescription = '' @@ -346,6 +384,17 @@ function resetRestrictIPValidationErrors() { restrictIPValidationErrors.value = [] } +function handleSelectAllProtocolsManually() { + // prevent selecting all protocols + if ( + protocols.value.length === supportedProtocols.value.length && + supportedProtocols.value.length > 0 + ) { + // remove the last selected protocol + protocols.value.pop() + } +} + function runFieldValidators( validators: validationOutput[], fieldName: string, @@ -399,8 +448,12 @@ function validate(): boolean { const sourceDestinationPortValidators: [validationOutput[], string, Ref][] = [ [ [ - // if destination port is present, source port is required - destinationPort.value ? validateRequired(sourcePort.value) : { valid: true }, + // source port required when destination is firewall or destination port is present + destinationAddressType.value === 'firewall' + ? validateRequired(sourcePort.value) + : destinationPort.value + ? validateRequired(sourcePort.value) + : { valid: true }, sourcePort.value ? validateAnyOf( [validatePort, validatePortRange], @@ -427,26 +480,38 @@ function validate(): boolean { ] ] - const destinationIpRequired = sourcePort.value == '' || destinationPort.value == '' + // if traffic type is 'select_protocols', at least one protocol must be selected from the dropdown + const protocolValidators: [validationOutput[], string, Ref][] = + trafficType.value === 'select_protocols' + ? [[[validateRequiredOption(protocols.value)], 'protocols', protocolsRef]] + : [] + + // destination address validation based on type + const destinationValidators: [validationOutput[], string, Ref][] = [] + + if (destinationAddressType.value === 'address') { + // type is 'address', IP is required and must be valid + destinationValidators.push([ + [validateRequired(destinationIP.value), validateIpAddress(destinationIP.value)], + 'destinationIP', + destinationIpRef + ]) + } else if (destinationAddressType.value === 'object') { + // type is 'object' + destinationValidators.push([ + [validateRequired(destinationAddressObject.value)], + 'destinationAddressObject', + destinationObjectRef + ]) + } + // if type is 'firewall', no validation needed as the destination address is the firewall's IP + // and the source port is checked above in sourceDestinationPortValidators const validators: [validationOutput[], string, Ref][] = [ [[validateRequired(name.value)], 'name', nameRef], - ...(anyProtocolSelected.value ? [] : sourceDestinationPortValidators), - destinationAddressType.value === 'address' - ? [ - destinationIpRequired - ? [validateRequired(destinationIP.value), validateIpAddress(destinationIP.value)] - : destinationIP.value - ? [validateIpAddress(destinationIP.value)] - : [{ valid: true }], - 'destinationIP', - destinationIpRef - ] - : [ - [validateRequired(destinationAddressObject.value)], - 'destinationAddressObject', - destinationObjectRef - ], + ...protocolValidators, + ...(trafficType.value === 'all_traffic' ? [] : sourceDestinationPortValidators), + ...destinationValidators, [ reflection.value ? [validateRequiredOption(reflectionZones.value)] : [], 'reflectionZones', @@ -478,11 +543,12 @@ async function createOrEditPortForward() { const payload: CreateEditPortForwardPayload = { dest_ip: '', ns_dst: '', - proto: anyProtocolSelected.value - ? ['all'] - : protocols.value.map((protoObj: NeComboboxOption) => protoObj.id), - src_dport: anyProtocolSelected.value ? '' : sourcePort.value, - dest_port: anyProtocolSelected.value ? '' : destinationPort.value, + proto: + trafficType.value === 'all_traffic' + ? ['all'] + : protocols.value.map((protoObj: NeComboboxOption) => protoObj.id), + src_dport: trafficType.value === 'all_traffic' ? '' : sourcePort.value, + dest_port: trafficType.value === 'all_traffic' ? '' : destinationPort.value, name: name.value, src_dip: wan.value === 'any' ? '' : wan.value, enabled: enabled.value ? '1' : '0', @@ -503,10 +569,11 @@ async function createOrEditPortForward() { if (destinationAddressType.value === 'address') { // destination address payload.dest_ip = destinationIP.value - } else { + } else if (destinationAddressType.value === 'object') { // destination object payload.ns_dst = destinationAddressObject.value } + // if destinationAddressType is 'firewall', dest_ip and ns_dst remain empty if (restrictType.value === 'address') { // restrict addresses @@ -583,11 +650,26 @@ async function createOrEditPortForward() { :disabled="isSubmittingRequest" :invalid-message="validationErrorBag.getFirstFor('name')" /> + + + + + - +