mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 10:14:35 +00:00
feat(ui): add ACL graphical configuration interface (#1815)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { AutoComplete, Button, Checkbox, Dialog, Divider, InputNumber, InputText, Panel, Password, SelectButton, ToggleButton } from 'primevue'
|
||||||
import InputGroup from 'primevue/inputgroup'
|
import InputGroup from 'primevue/inputgroup'
|
||||||
import InputGroupAddon from 'primevue/inputgroupaddon'
|
import InputGroupAddon from 'primevue/inputgroupaddon'
|
||||||
import { Checkbox, InputText, InputNumber, AutoComplete, Panel, Divider, ToggleButton, Button, Password, Dialog } from 'primevue'
|
|
||||||
import {
|
import {
|
||||||
addRow,
|
addRow,
|
||||||
DEFAULT_NETWORK_CONFIG,
|
DEFAULT_NETWORK_CONFIG,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../types/network'
|
} from '../types/network'
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import AclManager from './acl/AclManager.vue'
|
||||||
import UrlListInput from './UrlListInput.vue'
|
import UrlListInput from './UrlListInput.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -488,6 +489,18 @@ watch(() => curNetwork.value, syncNormalizedNetwork, { immediate: true, deep: fa
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Panel :header="t('acl.title')" toggleable collapsed>
|
||||||
|
<div v-if="curNetwork.acl" class="flex flex-col gap-y-2">
|
||||||
|
<AclManager v-model="curNetwork.acl" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex justify-center p-4">
|
||||||
|
<Button :label="t('acl.enabled')"
|
||||||
|
@click="curNetwork.acl = { acl_v1: { chains: [], group: { declares: [], members: [] } } }" />
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<div class="flex pt-6 justify-center">
|
<div class="flex pt-6 justify-center">
|
||||||
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
<Button :label="t('run_network')" icon="pi pi-arrow-right" icon-pos="right" :disabled="configInvalid"
|
||||||
@click="$emit('runNetwork', curNetwork)" />
|
@click="$emit('runNetwork', curNetwork)" />
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, Column, DataTable, Divider, InputText, Select, SelectButton, ToggleButton } from 'primevue'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { AclAction, AclChain, AclChainType, AclProtocol, AclRule } from '../../types/network'
|
||||||
|
import AclRuleDialog from './AclRuleDialog.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
groupNames?: string[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const chain = defineModel<AclChain>({ required: true })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
watch(() => chain.value.rules, (newRules) => {
|
||||||
|
if (!newRules) return
|
||||||
|
const isSorted = newRules.every((rule, i) => i === 0 || (rule.priority || 0) <= (newRules[i - 1].priority || 0))
|
||||||
|
if (!isSorted) {
|
||||||
|
chain.value.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||||
|
}
|
||||||
|
}, { deep: true, immediate: true })
|
||||||
|
|
||||||
|
const actionOptions = [
|
||||||
|
{ label: () => t('acl.allow'), value: AclAction.Allow },
|
||||||
|
{ label: () => t('acl.drop'), value: AclAction.Drop },
|
||||||
|
]
|
||||||
|
|
||||||
|
const chainTypeOptions = [
|
||||||
|
{ label: () => t('acl.inbound'), value: AclChainType.Inbound },
|
||||||
|
{ label: () => t('acl.outbound'), value: AclChainType.Outbound },
|
||||||
|
{ label: () => t('acl.forward'), value: AclChainType.Forward },
|
||||||
|
]
|
||||||
|
|
||||||
|
const editingRule = ref<AclRule | null>(null)
|
||||||
|
const editingRuleIndex = ref(-1)
|
||||||
|
const showRuleDialog = ref(false)
|
||||||
|
|
||||||
|
function getProtocolLabel(proto: AclProtocol) {
|
||||||
|
switch (proto) {
|
||||||
|
case AclProtocol.Any: return t('acl.any')
|
||||||
|
case AclProtocol.TCP: return 'TCP'
|
||||||
|
case AclProtocol.UDP: return 'UDP'
|
||||||
|
case AclProtocol.ICMP: return 'ICMP'
|
||||||
|
case AclProtocol.ICMPv6: return 'ICMPv6'
|
||||||
|
default: return t('event.Unknown')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionLabel(action: AclAction) {
|
||||||
|
switch (action) {
|
||||||
|
case AclAction.Allow: return t('acl.allow')
|
||||||
|
case AclAction.Drop: return t('acl.drop')
|
||||||
|
default: return t('event.Unknown')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRule() {
|
||||||
|
editingRuleIndex.value = -1
|
||||||
|
editingRule.value = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
priority: chain.value.rules.length,
|
||||||
|
enabled: true,
|
||||||
|
protocol: AclProtocol.Any,
|
||||||
|
ports: [],
|
||||||
|
source_ips: [],
|
||||||
|
destination_ips: [],
|
||||||
|
source_ports: [],
|
||||||
|
action: AclAction.Allow,
|
||||||
|
rate_limit: 0,
|
||||||
|
burst_limit: 0,
|
||||||
|
stateful: false,
|
||||||
|
source_groups: [],
|
||||||
|
destination_groups: [],
|
||||||
|
}
|
||||||
|
showRuleDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function editRule(index: number) {
|
||||||
|
editingRuleIndex.value = index
|
||||||
|
editingRule.value = JSON.parse(JSON.stringify(chain.value.rules[index]))
|
||||||
|
showRuleDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRule(index: number) {
|
||||||
|
chain.value.rules.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRule(rule: AclRule) {
|
||||||
|
if (editingRuleIndex.value === -1) {
|
||||||
|
chain.value.rules.push(rule)
|
||||||
|
} else {
|
||||||
|
chain.value.rules[editingRuleIndex.value] = rule
|
||||||
|
}
|
||||||
|
chain.value.rules.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRowReorder(event: any) {
|
||||||
|
chain.value.rules = event.value
|
||||||
|
// Update priorities based on new order (higher priority at top)
|
||||||
|
chain.value.rules.forEach((rule, index) => {
|
||||||
|
rule.priority = chain.value.rules.length - index - 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<!-- Chain Metadata Section -->
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-200 dark:bg-gray-900 dark:border-gray-700">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold text-sm">{{ t('acl.chain.name') }}</label>
|
||||||
|
<InputText v-model="chain.name" size="small" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold text-sm">{{ t('acl.rule.description') }}</label>
|
||||||
|
<InputText v-model="chain.description" size="small" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-6 col-span-full border-t pt-2 mt-2 dark:border-gray-700">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="font-bold text-sm">{{ t('acl.rule.enabled') }}</label>
|
||||||
|
<ToggleButton v-model="chain.enabled" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||||
|
:on-label="t('web.common.enable')" :off-label="t('web.common.disable')" class="w-24" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="font-bold text-sm">{{ t('acl.chain.type') }}</label>
|
||||||
|
<Select v-model="chain.chain_type" :options="chainTypeOptions" :option-label="opt => opt.label()"
|
||||||
|
option-value="value" size="small" class="w-40" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 ml-auto">
|
||||||
|
<label class="font-bold text-sm">{{ t('acl.default_action') }}</label>
|
||||||
|
<SelectButton v-model="chain.default_action" :options="actionOptions" :option-label="opt => opt.label()"
|
||||||
|
option-value="value" :allow-empty="false" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center gap-4 justify-between">
|
||||||
|
<h4 class="text-md font-bold">{{ t('acl.rules') }}</h4>
|
||||||
|
<Button icon="pi pi-plus" :label="t('acl.add_rule')" severity="success" size="small" @click="addRule" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable :value="chain.rules" @row-reorder="onRowReorder" responsiveLayout="scroll">
|
||||||
|
<Column rowReorder headerStyle="width: 3rem" />
|
||||||
|
<Column field="enabled" :header="t('acl.rule.enabled')">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<i class="pi" :class="data.enabled ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'"></i>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="name" :header="t('acl.rule.name')" />
|
||||||
|
<Column :header="t('acl.match')">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col gap-2 py-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded-md text-[10px] font-bold uppercase tracking-wider">
|
||||||
|
{{ getProtocolLabel(data.protocol) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3">
|
||||||
|
<div class="flex items-center gap-1.5 min-w-0">
|
||||||
|
<span class="text-[10px] font-bold text-gray-400 uppercase w-7">Src</span>
|
||||||
|
<div class="flex flex-wrap gap-1 items-center overflow-hidden">
|
||||||
|
<span v-for="ip in data.source_ips" :key="ip"
|
||||||
|
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded">{{ ip }}</span>
|
||||||
|
<span v-for="grp in data.source_groups" :key="grp"
|
||||||
|
class="text-xs font-bold text-purple-600 dark:text-purple-400">@{{ grp }}</span>
|
||||||
|
<span v-if="data.source_ports.length" class="text-xs text-blue-600 dark:text-blue-400 font-mono">:{{
|
||||||
|
data.source_ports.join(',') }}</span>
|
||||||
|
<span v-if="!data.source_ips.length && !data.source_groups.length" class="text-gray-400">*</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<i class="pi pi-arrow-right hidden sm:block text-gray-300 text-xs"></i>
|
||||||
|
<Divider layout="horizontal" class="sm:hidden my-1" />
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5 min-w-0">
|
||||||
|
<span class="text-[10px] font-bold text-gray-400 uppercase w-7">Dst</span>
|
||||||
|
<div class="flex flex-wrap gap-1 items-center overflow-hidden">
|
||||||
|
<span v-for="ip in data.destination_ips" :key="ip"
|
||||||
|
class="font-mono text-xs bg-surface-100 dark:bg-surface-800 px-1.5 py-0.5 rounded">{{ ip }}</span>
|
||||||
|
<span v-for="grp in data.destination_groups" :key="grp"
|
||||||
|
class="text-xs font-bold text-purple-600 dark:text-purple-400">@{{ grp }}</span>
|
||||||
|
<span v-if="data.ports.length" class="text-xs text-blue-600 dark:text-blue-400 font-mono">:{{
|
||||||
|
data.ports.join(',') }}</span>
|
||||||
|
<span v-if="!data.destination_ips.length && !data.destination_groups.length"
|
||||||
|
class="text-gray-400">*</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="action" :header="t('acl.rule.action')">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span :class="data.action === AclAction.Allow ? 'text-green-600' : 'text-red-600 font-bold'">
|
||||||
|
{{ getActionLabel(data.action) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column :header="t('web.common.edit')">
|
||||||
|
<template #body="{ index }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button icon="pi pi-pencil" text rounded @click="editRule(index)" />
|
||||||
|
<Button icon="pi pi-trash" severity="danger" text rounded @click="deleteRule(index)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<AclRuleDialog v-if="showRuleDialog && editingRule" v-model:visible="showRuleDialog" v-model:rule="editingRule"
|
||||||
|
:group-names="props.groupNames" @save="saveRule" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, Column, DataTable, Dialog, InputText, MultiSelect, Password } from 'primevue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { GroupIdentity, GroupInfo } from '../../types/network';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
groupNames?: string[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const group = defineModel<GroupInfo>({ required: true })
|
||||||
|
const emit = defineEmits(['rename-group'])
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const editingGroup = ref<GroupIdentity | null>(null)
|
||||||
|
const editingGroupIndex = ref(-1)
|
||||||
|
const showGroupDialog = ref(false)
|
||||||
|
const oldGroupName = ref('')
|
||||||
|
|
||||||
|
function addGroup() {
|
||||||
|
editingGroupIndex.value = -1
|
||||||
|
editingGroup.value = {
|
||||||
|
group_name: '',
|
||||||
|
group_secret: '',
|
||||||
|
}
|
||||||
|
oldGroupName.value = ''
|
||||||
|
showGroupDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function editGroup(index: number) {
|
||||||
|
editingGroupIndex.value = index
|
||||||
|
editingGroup.value = JSON.parse(JSON.stringify(group.value.declares[index]))
|
||||||
|
oldGroupName.value = editingGroup.value?.group_name || ''
|
||||||
|
showGroupDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteGroup(index: number) {
|
||||||
|
group.value.declares.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGroup() {
|
||||||
|
if (!editingGroup.value) return
|
||||||
|
const newName = editingGroup.value.group_name
|
||||||
|
|
||||||
|
if (editingGroupIndex.value === -1) {
|
||||||
|
group.value.declares.push(editingGroup.value)
|
||||||
|
} else {
|
||||||
|
if (oldGroupName.value && oldGroupName.value !== newName) {
|
||||||
|
// Sync in members
|
||||||
|
group.value.members = group.value.members.map(m => m === oldGroupName.value ? newName : m)
|
||||||
|
// Notify parent to sync in rules
|
||||||
|
emit('rename-group', { oldName: oldGroupName.value, newName })
|
||||||
|
}
|
||||||
|
group.value.declares[editingGroupIndex.value] = editingGroup.value
|
||||||
|
}
|
||||||
|
showGroupDialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="font-bold text-lg">{{ t('acl.group.declares') }}</label>
|
||||||
|
<small class="text-gray-500">{{ t('acl.group.help') }}</small>
|
||||||
|
</div>
|
||||||
|
<Button icon="pi pi-plus" :label="t('web.common.add')" severity="success" @click="addGroup" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable :value="group.declares" responsiveLayout="scroll">
|
||||||
|
<Column field="group_name" :header="t('acl.group.name')" />
|
||||||
|
<Column field="group_secret" :header="t('acl.group.secret')">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Password v-model="data.group_secret" :feedback="false" toggleMask readonly plain class="w-full" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column :header="t('web.common.edit')" headerStyle="width: 8rem">
|
||||||
|
<template #body="{ index }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button icon="pi pi-pencil" text rounded @click="editGroup(index)" />
|
||||||
|
<Button icon="pi pi-trash" severity="danger" text rounded @click="deleteGroup(index)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold text-lg">{{ t('acl.group.members') }}</label>
|
||||||
|
<MultiSelect v-model="group.members" :options="props.groupNames" multiple fluid filter
|
||||||
|
:placeholder="t('acl.group.members')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group Identity Dialog -->
|
||||||
|
<Dialog v-model:visible="showGroupDialog" modal :header="t('acl.groups')" :style="{ width: '400px' }">
|
||||||
|
<div v-if="editingGroup" class="flex flex-col gap-4 pt-2">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold">{{ t('acl.group.name') }}</label>
|
||||||
|
<InputText v-model="editingGroup.group_name" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold">{{ t('acl.group.secret') }}</label>
|
||||||
|
<Password v-model="editingGroup.group_secret" :feedback="false" toggleMask fluid />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="showGroupDialog = false" text />
|
||||||
|
<Button :label="t('web.common.save')" icon="pi pi-save" @click="saveGroup" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, Menu, Tab, TabList, TabPanel, TabPanels, Tabs } from 'primevue'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Acl, AclAction, AclChainType } from '../../types/network'
|
||||||
|
import AclChainEditor from './AclChainEditor.vue'
|
||||||
|
import AclGroupEditor from './AclGroupEditor.vue'
|
||||||
|
|
||||||
|
const acl = defineModel<Acl>({ required: true })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const activeTab = ref(0)
|
||||||
|
const menu = ref()
|
||||||
|
|
||||||
|
const addMenuModel = ref([
|
||||||
|
{ label: () => t('acl.inbound'), command: () => addChain(AclChainType.Inbound) },
|
||||||
|
{ label: () => t('acl.outbound'), command: () => addChain(AclChainType.Outbound) },
|
||||||
|
{ label: () => t('acl.forward'), command: () => addChain(AclChainType.Forward) },
|
||||||
|
])
|
||||||
|
|
||||||
|
function addChain(type: AclChainType) {
|
||||||
|
if (!acl.value.acl_v1) {
|
||||||
|
acl.value.acl_v1 = { chains: [], group: { declares: [], members: [] } }
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultName = ''
|
||||||
|
switch (type) {
|
||||||
|
case AclChainType.Inbound: defaultName = 'Inbound'; break;
|
||||||
|
case AclChainType.Outbound: defaultName = 'Outbound'; break;
|
||||||
|
case AclChainType.Forward: defaultName = 'Forward'; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
acl.value.acl_v1.chains.push({
|
||||||
|
name: defaultName,
|
||||||
|
chain_type: type,
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
rules: [],
|
||||||
|
default_action: AclAction.Allow
|
||||||
|
})
|
||||||
|
|
||||||
|
activeTab.value = acl.value.acl_v1.chains.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChain(index: number) {
|
||||||
|
if (confirm(t('acl.delete_chain_confirm'))) {
|
||||||
|
acl.value.acl_v1?.chains.splice(index, 1)
|
||||||
|
if (activeTab.value >= (acl.value.acl_v1?.chains.length || 0)) {
|
||||||
|
activeTab.value = Math.max(0, (acl.value.acl_v1?.chains.length || 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRenameGroup({ oldName, newName }: { oldName: string, newName: string }) {
|
||||||
|
if (!acl.value.acl_v1) return
|
||||||
|
acl.value.acl_v1.chains.forEach(chain => {
|
||||||
|
chain.rules.forEach(rule => {
|
||||||
|
rule.source_groups = rule.source_groups.map(g => g === oldName ? newName : g)
|
||||||
|
rule.destination_groups = rule.destination_groups.map(g => g === oldName ? newName : g)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupNames = computed(() => {
|
||||||
|
return acl.value.acl_v1?.group?.declares.map(g => g.group_name) || []
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = computed(() => {
|
||||||
|
const chains = acl.value.acl_v1?.chains || []
|
||||||
|
const result: { type: string, label: string, index: number }[] = []
|
||||||
|
|
||||||
|
if (chains.length === 0) {
|
||||||
|
result.push({ type: 'empty', label: t('acl.chains'), index: 0 })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
chains.forEach((c, index) => {
|
||||||
|
result.push({
|
||||||
|
type: 'chain',
|
||||||
|
label: c.name || `Chain ${index}`,
|
||||||
|
index
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({ type: 'groups', label: t('acl.groups'), index: result.length })
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<Tabs v-model:value="activeTab">
|
||||||
|
<div class="flex items-center border-b border-surface-200 dark:border-surface-700">
|
||||||
|
<TabList class="flex-grow min-w-0 overflow-x-auto" style="border-bottom: none;">
|
||||||
|
<Tab v-for="tab in tabs" :key="tab.type + tab.index" :value="tab.index">
|
||||||
|
<div class="flex items-center gap-2 whitespace-nowrap">
|
||||||
|
{{ tab.label }}
|
||||||
|
<Button v-if="tab.type === 'chain'" icon="pi pi-times" severity="danger" text rounded size="small"
|
||||||
|
class="w-6 h-6 p-0" @click.stop="removeChain(tab.index)" />
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</TabList>
|
||||||
|
<div
|
||||||
|
class="flex-shrink-0 flex items-center px-2 bg-white dark:bg-gray-900 border-l border-surface-100 dark:border-surface-800">
|
||||||
|
<Button icon="pi pi-plus" text rounded size="small" class="w-8 h-8 p-0"
|
||||||
|
@click="(event) => menu.toggle(event)" />
|
||||||
|
<Menu ref="menu" :model="addMenuModel" :popup="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel v-for="tab in tabs" :key="'panel' + tab.type + tab.index" :value="tab.index">
|
||||||
|
<!-- Empty State within TabPanel -->
|
||||||
|
<div v-if="tab.type === 'empty'"
|
||||||
|
class="py-8 flex flex-col items-center justify-center border-2 border-dashed border-surface-200 rounded-lg bg-surface-50 dark:bg-surface-900 dark:border-surface-700">
|
||||||
|
<i class="pi pi-shield text-5xl mb-4 text-primary" />
|
||||||
|
<div class="text-xl font-bold mb-2">{{ t('acl.chains') }}</div>
|
||||||
|
<p class="text-surface-500 mb-8 text-center max-w-sm px-4">{{ t('acl.help') }}</p>
|
||||||
|
<div class="flex flex-wrap gap-3 justify-center">
|
||||||
|
<Button :label="t('acl.inbound')" icon="pi pi-arrow-down-left" @click="addChain(AclChainType.Inbound)" />
|
||||||
|
<Button :label="t('acl.outbound')" icon="pi pi-arrow-up-right" @click="addChain(AclChainType.Outbound)" />
|
||||||
|
<Button :label="t('acl.forward')" icon="pi pi-directions" @click="addChain(AclChainType.Forward)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rule Chains -->
|
||||||
|
<div v-if="tab.type === 'chain' && acl.acl_v1 && acl.acl_v1.chains[tab.index]" class="py-4">
|
||||||
|
<AclChainEditor v-model="acl.acl_v1.chains[tab.index]" :group-names="groupNames" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group Management -->
|
||||||
|
<div v-if="tab.type === 'groups'" class="py-4">
|
||||||
|
<template v-if="acl.acl_v1">
|
||||||
|
<AclGroupEditor v-if="acl.acl_v1.group" v-model="acl.acl_v1.group" :group-names="groupNames"
|
||||||
|
@rename-group="handleRenameGroup" />
|
||||||
|
<div v-else class="flex justify-center p-4">
|
||||||
|
<Button :label="t('web.common.add') + ' ' + t('acl.groups')"
|
||||||
|
@click="acl.acl_v1.group = { declares: [], members: [] }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="flex justify-center p-4">
|
||||||
|
<Button :label="t('acl.enabled')"
|
||||||
|
@click="acl.acl_v1 = { chains: [], group: { declares: [], members: [] } }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AutoComplete, Button, Checkbox, Dialog, InputNumber, InputText, MultiSelect, Panel, SelectButton, ToggleButton } from 'primevue';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { AclAction, AclProtocol, AclRule } from '../../types/network';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
groupNames?: string[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'save'])
|
||||||
|
|
||||||
|
const rule = defineModel<AclRule>('rule', { required: true })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const protocolOptions = [
|
||||||
|
{ label: () => t('acl.any'), value: AclProtocol.Any },
|
||||||
|
{ label: 'TCP', value: AclProtocol.TCP },
|
||||||
|
{ label: 'UDP', value: AclProtocol.UDP },
|
||||||
|
{ label: 'ICMP', value: AclProtocol.ICMP },
|
||||||
|
{ label: 'ICMPv6', value: AclProtocol.ICMPv6 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const actionOptions = [
|
||||||
|
{ label: () => t('acl.allow'), value: AclAction.Allow },
|
||||||
|
{ label: () => t('acl.drop'), value: AclAction.Drop },
|
||||||
|
]
|
||||||
|
|
||||||
|
const showPorts = computed(() => {
|
||||||
|
return rule.value.protocol === AclProtocol.TCP || rule.value.protocol === AclProtocol.UDP || rule.value.protocol === AclProtocol.Any
|
||||||
|
})
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
emit('save', rule.value)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestions for IP/Port AutoComplete
|
||||||
|
const genericSuggestions = ref<string[]>([])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog :visible="visible" @update:visible="emit('update:visible', $event)" modal :header="t('acl.edit_rule')"
|
||||||
|
:style="{ width: '90vw', maxWidth: '600px' }">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-row gap-4 items-center">
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.name') }}</label>
|
||||||
|
<InputText v-model="rule.name" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.enabled') }}</label>
|
||||||
|
<ToggleButton v-model="rule.enabled" on-icon="pi pi-check" off-icon="pi pi-times"
|
||||||
|
:on-label="t('web.common.enable')" :off-label="t('web.common.disable')" class="w-24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.description') }}</label>
|
||||||
|
<InputText v-model="rule.description" fluid />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-4 flex-wrap">
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.action') }}</label>
|
||||||
|
<SelectButton v-model="rule.action" :options="actionOptions" :option-label="opt => opt.label()"
|
||||||
|
option-value="value" :allow-empty="false" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.protocol') }}</label>
|
||||||
|
<SelectButton v-model="rule.protocol" :options="protocolOptions"
|
||||||
|
:option-label="opt => typeof opt.label === 'function' ? opt.label() : opt.label" option-value="value"
|
||||||
|
:allow-empty="false" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Panel :header="t('acl.rules')" toggleable>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.src_ips') }}</label>
|
||||||
|
<AutoComplete v-model="rule.source_ips" multiple fluid :suggestions="genericSuggestions"
|
||||||
|
@complete="genericSuggestions = [$event.query]"
|
||||||
|
:placeholder="t('chips_placeholder', ['10.126.126.0/24'])" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.dst_ips') }}</label>
|
||||||
|
<AutoComplete v-model="rule.destination_ips" multiple fluid :suggestions="genericSuggestions"
|
||||||
|
@complete="genericSuggestions = [$event.query]"
|
||||||
|
:placeholder="t('chips_placeholder', ['10.126.126.2/32'])" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showPorts" class="flex flex-row gap-4 flex-wrap">
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.src_ports') }}</label>
|
||||||
|
<AutoComplete v-model="rule.source_ports" multiple fluid :suggestions="genericSuggestions"
|
||||||
|
@complete="genericSuggestions = [$event.query]" placeholder="e.g. 80, 1000-2000" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.dst_ports') }}</label>
|
||||||
|
<AutoComplete v-model="rule.ports" multiple fluid :suggestions="genericSuggestions"
|
||||||
|
@complete="genericSuggestions = [$event.query]" placeholder="e.g. 80, 1000-2000" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel :header="t('advanced_settings')" toggleable collapsed>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox v-model="rule.stateful" :binary="true" inputId="rule-stateful" />
|
||||||
|
<label for="rule-stateful" class="font-bold">{{ t('acl.rule.stateful') }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-4 flex-wrap">
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.rate_limit') }}</label>
|
||||||
|
<InputNumber v-model="rule.rate_limit" :min="0" placeholder="0 = no limit" fluid />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.burst_limit') }}</label>
|
||||||
|
<InputNumber v-model="rule.burst_limit" :min="0" placeholder="0 = no limit" fluid />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.src_groups') }}</label>
|
||||||
|
<MultiSelect v-model="rule.source_groups" :options="props.groupNames" multiple fluid filter
|
||||||
|
:placeholder="t('acl.rule.src_groups')" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="font-bold">{{ t('acl.rule.dst_groups') }}</label>
|
||||||
|
<MultiSelect v-model="rule.destination_groups" :options="props.groupNames" multiple fluid filter
|
||||||
|
:placeholder="t('acl.rule.dst_groups')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="close" text />
|
||||||
|
<Button :label="t('web.common.save')" icon="pi pi-save" @click="save" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -355,6 +355,7 @@ web:
|
|||||||
delete: 删除
|
delete: 删除
|
||||||
edit: 编辑
|
edit: 编辑
|
||||||
refresh: 刷新
|
refresh: 刷新
|
||||||
|
add: 添加
|
||||||
loading: 加载中...
|
loading: 加载中...
|
||||||
error: 错误
|
error: 错误
|
||||||
success: 成功
|
success: 成功
|
||||||
@@ -422,3 +423,46 @@ config-server:
|
|||||||
client:
|
client:
|
||||||
not_running: 无法连接至远程客户端
|
not_running: 无法连接至远程客户端
|
||||||
retry: 重试
|
retry: 重试
|
||||||
|
|
||||||
|
acl:
|
||||||
|
title: 访问控制
|
||||||
|
help: 访问控制列表,用于限制节点间的通信。
|
||||||
|
enabled: 启用 ACL
|
||||||
|
default_action: 默认动作
|
||||||
|
chains: 规则链
|
||||||
|
inbound: 入站
|
||||||
|
outbound: 出站
|
||||||
|
forward: 转发
|
||||||
|
rules: 规则
|
||||||
|
add_rule: 添加规则
|
||||||
|
edit_rule: 编辑规则
|
||||||
|
rule:
|
||||||
|
name: 规则名称
|
||||||
|
description: 描述
|
||||||
|
enabled: 启用
|
||||||
|
protocol: 协议
|
||||||
|
action: 动作
|
||||||
|
src_ips: 来源 IP
|
||||||
|
dst_ips: 目的 IP
|
||||||
|
src_ports: 来源端口
|
||||||
|
dst_ports: 目的端口
|
||||||
|
rate_limit: 速率限制 (pps)
|
||||||
|
burst_limit: 爆发限制
|
||||||
|
stateful: 状态追踪
|
||||||
|
src_groups: 来源组
|
||||||
|
dst_groups: 目的组
|
||||||
|
groups: 组管理
|
||||||
|
group:
|
||||||
|
declares: 声明组
|
||||||
|
members: 加入组
|
||||||
|
name: 组名
|
||||||
|
secret: 密钥
|
||||||
|
help: 在此处定义网络中的组身份,以便在规则中使用。
|
||||||
|
any: 任意
|
||||||
|
allow: 允许
|
||||||
|
drop: 丢弃
|
||||||
|
delete_chain_confirm: 确定要删除此规则链及其所有规则吗?
|
||||||
|
chain:
|
||||||
|
name: 名称
|
||||||
|
type: 类型
|
||||||
|
match: 匹配
|
||||||
|
|||||||
@@ -355,6 +355,7 @@ web:
|
|||||||
delete: Delete
|
delete: Delete
|
||||||
edit: Edit
|
edit: Edit
|
||||||
refresh: Refresh
|
refresh: Refresh
|
||||||
|
add: Add
|
||||||
loading: Loading...
|
loading: Loading...
|
||||||
error: Error
|
error: Error
|
||||||
success: Success
|
success: Success
|
||||||
@@ -422,3 +423,46 @@ config-server:
|
|||||||
client:
|
client:
|
||||||
not_running: Unable to connect to remote client.
|
not_running: Unable to connect to remote client.
|
||||||
retry: Retry
|
retry: Retry
|
||||||
|
|
||||||
|
acl:
|
||||||
|
title: Access Control (ACL)
|
||||||
|
help: Access control list to restrict communication between nodes.
|
||||||
|
enabled: Enable ACL
|
||||||
|
default_action: Default Action
|
||||||
|
chains: Rule Chains
|
||||||
|
inbound: Inbound
|
||||||
|
outbound: Outbound
|
||||||
|
forward: Forward
|
||||||
|
rules: Rules
|
||||||
|
add_rule: Add Rule
|
||||||
|
edit_rule: Edit Rule
|
||||||
|
rule:
|
||||||
|
name: Rule Name
|
||||||
|
description: Description
|
||||||
|
enabled: Enabled
|
||||||
|
protocol: Protocol
|
||||||
|
action: Action
|
||||||
|
src_ips: Source IPs
|
||||||
|
dst_ips: Destination IPs
|
||||||
|
src_ports: Source Ports
|
||||||
|
dst_ports: Destination Ports
|
||||||
|
rate_limit: Rate Limit (pps)
|
||||||
|
burst_limit: Burst Limit
|
||||||
|
stateful: Stateful
|
||||||
|
src_groups: Source Groups
|
||||||
|
dst_groups: Destination Groups
|
||||||
|
groups: Groups
|
||||||
|
group:
|
||||||
|
declares: Declared Groups
|
||||||
|
members: Node Memberships
|
||||||
|
name: Group Name
|
||||||
|
secret: Group Secret
|
||||||
|
help: Define group identities in the network to use them in rules.
|
||||||
|
any: Any
|
||||||
|
allow: Allow
|
||||||
|
drop: Drop
|
||||||
|
delete_chain_confirm: Are you sure you want to delete this rule chain and all its rules?
|
||||||
|
chain:
|
||||||
|
name: Name
|
||||||
|
type: Type
|
||||||
|
match: Match
|
||||||
|
|||||||
@@ -14,6 +14,74 @@ export interface SecureModeConfig {
|
|||||||
local_public_key?: string
|
local_public_key?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AclProtocol {
|
||||||
|
Unspecified = 0,
|
||||||
|
TCP = 1,
|
||||||
|
UDP = 2,
|
||||||
|
ICMP = 3,
|
||||||
|
ICMPv6 = 4,
|
||||||
|
Any = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AclAction {
|
||||||
|
Noop = 0,
|
||||||
|
Allow = 1,
|
||||||
|
Drop = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AclChainType {
|
||||||
|
UnspecifiedChain = 0,
|
||||||
|
Inbound = 1,
|
||||||
|
Outbound = 2,
|
||||||
|
Forward = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AclRule {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
priority: number
|
||||||
|
enabled: boolean
|
||||||
|
protocol: AclProtocol
|
||||||
|
ports: string[]
|
||||||
|
source_ips: string[]
|
||||||
|
destination_ips: string[]
|
||||||
|
source_ports: string[]
|
||||||
|
action: AclAction
|
||||||
|
rate_limit: number
|
||||||
|
burst_limit: number
|
||||||
|
stateful: boolean
|
||||||
|
source_groups: string[]
|
||||||
|
destination_groups: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AclChain {
|
||||||
|
name: string
|
||||||
|
chain_type: AclChainType
|
||||||
|
description: string
|
||||||
|
enabled: boolean
|
||||||
|
rules: AclRule[]
|
||||||
|
default_action: AclAction
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupIdentity {
|
||||||
|
group_name: string
|
||||||
|
group_secret: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupInfo {
|
||||||
|
declares: GroupIdentity[]
|
||||||
|
members: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AclV1 {
|
||||||
|
chains: AclChain[]
|
||||||
|
group?: GroupInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Acl {
|
||||||
|
acl_v1?: AclV1
|
||||||
|
}
|
||||||
|
|
||||||
export interface NetworkConfig {
|
export interface NetworkConfig {
|
||||||
instance_id: string
|
instance_id: string
|
||||||
|
|
||||||
@@ -85,6 +153,7 @@ export interface NetworkConfig {
|
|||||||
enable_private_mode?: boolean
|
enable_private_mode?: boolean
|
||||||
|
|
||||||
port_forwards: PortForwardConfig[]
|
port_forwards: PortForwardConfig[]
|
||||||
|
acl?: Acl
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
||||||
@@ -152,6 +221,15 @@ export function DEFAULT_NETWORK_CONFIG(): NetworkConfig {
|
|||||||
enable_magic_dns: false,
|
enable_magic_dns: false,
|
||||||
enable_private_mode: false,
|
enable_private_mode: false,
|
||||||
port_forwards: [],
|
port_forwards: [],
|
||||||
|
acl: {
|
||||||
|
acl_v1: {
|
||||||
|
group: {
|
||||||
|
declares: [],
|
||||||
|
members: [],
|
||||||
|
},
|
||||||
|
chains: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -825,6 +825,12 @@ impl NetworkConfig {
|
|||||||
flags.encryption_algorithm = encryption_algorithm;
|
flags.encryption_algorithm = encryption_algorithm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(acl) = self.acl.as_ref()
|
||||||
|
&& !acl.is_empty()
|
||||||
|
{
|
||||||
|
cfg.set_acl(Some(acl.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(data_compress_algo) = self.data_compress_algo {
|
if let Some(data_compress_algo) = self.data_compress_algo {
|
||||||
if data_compress_algo < 1 {
|
if data_compress_algo < 1 {
|
||||||
flags.data_compress_algo = 1;
|
flags.data_compress_algo = 1;
|
||||||
@@ -964,6 +970,8 @@ impl NetworkConfig {
|
|||||||
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
|
(flags.instance_recv_bps_limit != u64::MAX).then_some(flags.instance_recv_bps_limit);
|
||||||
result.enable_private_mode = Some(flags.private_mode);
|
result.enable_private_mode = Some(flags.private_mode);
|
||||||
|
|
||||||
|
result.acl = config.get_acl();
|
||||||
|
|
||||||
if flags.relay_network_whitelist == "*" {
|
if flags.relay_network_whitelist == "*" {
|
||||||
result.enable_relay_network_whitelist = Some(false);
|
result.enable_relay_network_whitelist = Some(false);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,26 @@ use std::fmt::Display;
|
|||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/acl.rs"));
|
include!(concat!(env!("OUT_DIR"), "/acl.rs"));
|
||||||
|
|
||||||
|
impl Acl {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.acl_v1.as_ref().map(|v1| v1.is_empty()).unwrap_or(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AclV1 {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
let has_chains = !self.chains.is_empty();
|
||||||
|
let has_groups = self.group.as_ref().map(|g| !g.is_empty()).unwrap_or(false);
|
||||||
|
!has_chains && !has_groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GroupInfo {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.declares.is_empty() && self.members.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for ConnTrackEntry {
|
impl Display for ConnTrackEntry {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let src = self
|
let src = self
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ syntax = "proto3";
|
|||||||
import "common.proto";
|
import "common.proto";
|
||||||
import "peer_rpc.proto";
|
import "peer_rpc.proto";
|
||||||
import "api_instance.proto";
|
import "api_instance.proto";
|
||||||
|
import "acl.proto";
|
||||||
|
|
||||||
package api.manage;
|
package api.manage;
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ message NetworkConfig {
|
|||||||
optional bool disable_tcp_hole_punching = 54;
|
optional bool disable_tcp_hole_punching = 54;
|
||||||
|
|
||||||
common.SecureModeConfig secure_mode = 55;
|
common.SecureModeConfig secure_mode = 55;
|
||||||
reserved 56;
|
optional acl.Acl acl = 56;
|
||||||
optional string credential_file = 57;
|
optional string credential_file = 57;
|
||||||
optional bool lazy_p2p = 58;
|
optional bool lazy_p2p = 58;
|
||||||
optional bool need_p2p = 59;
|
optional bool need_p2p = 59;
|
||||||
|
|||||||
Reference in New Issue
Block a user