1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: SSH 支持同时监听 IPv4 及 IPv6 (#3381)

Refs #3359
This commit is contained in:
ssongliu 2023-12-19 16:20:06 +08:00 committed by GitHub
parent b9c5fee411
commit 6b491710c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 138 additions and 63 deletions

View File

@ -48,7 +48,7 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
Status: constant.StatusEnable,
Message: "",
Port: "22",
ListenAddress: "0.0.0.0",
ListenAddress: "",
PasswordAuthentication: "yes",
PubkeyAuthentication: "yes",
PermitRootLogin: "yes",
@ -90,7 +90,12 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
data.Port = strings.ReplaceAll(line, "Port ", "")
}
if strings.HasPrefix(line, "ListenAddress ") {
data.ListenAddress = strings.ReplaceAll(line, "ListenAddress ", "")
itemAddr := strings.ReplaceAll(line, "ListenAddress ", "")
if len(data.ListenAddress) != 0 {
data.ListenAddress += ("," + itemAddr)
} else {
data.ListenAddress = itemAddr
}
}
if strings.HasPrefix(line, "PasswordAuthentication ") {
data.PasswordAuthentication = strings.ReplaceAll(line, "PasswordAuthentication ", "")
@ -366,32 +371,33 @@ func sortFileList(fileNames []sshFileItem) []sshFileItem {
return fileNames
}
func updateSSHConf(oldFiles []string, param string, value interface{}) []string {
hasKey := false
func updateSSHConf(oldFiles []string, param string, value string) []string {
var valueItems []string
if param != "ListenAddress" {
valueItems = append(valueItems, value)
} else {
if value != "" {
valueItems = strings.Split(value, ",")
}
}
var newFiles []string
for _, line := range oldFiles {
if strings.HasPrefix(line, param+" ") {
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value))
hasKey = true
lineItem := strings.TrimSpace(line)
if (strings.HasPrefix(lineItem, param) || strings.HasPrefix(lineItem, fmt.Sprintf("#%s", param))) && len(valueItems) != 0 {
newFiles = append(newFiles, fmt.Sprintf("%s %s", param, valueItems[0]))
valueItems = valueItems[1:]
continue
}
if strings.HasPrefix(lineItem, param) && len(valueItems) == 0 {
newFiles = append(newFiles, fmt.Sprintf("#%s", line))
continue
}
newFiles = append(newFiles, line)
}
if !hasKey {
newFiles = []string{}
for _, line := range oldFiles {
if strings.HasPrefix(line, fmt.Sprintf("#%s ", param)) && !hasKey {
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value))
hasKey = true
continue
if len(valueItems) != 0 {
for _, item := range valueItems {
newFiles = append(newFiles, fmt.Sprintf("%s %s", param, item))
}
newFiles = append(newFiles, line)
}
}
if !hasKey {
newFiles = []string{}
newFiles = append(newFiles, oldFiles...)
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value))
}
return newFiles
}

View File

@ -1120,8 +1120,10 @@ const message = {
'Modifying the configuration file may cause service availability. Exercise caution when performing this operation. Do you want to continue?',
portHelper: 'Specifies the port number monitored by the SSH service. The default port number is 22.',
listenAddress: 'Listening address',
addressHelper:
'Specify the IP address monitored by the SSH service. The default value is 0.0.0.0. That is, all network interfaces are monitored.',
allV4V6: '0.0.0.0:{0}(IPv4) and :::{0}(IPv6)',
listenHelper:
'Canceling IPv4 and IPv6 settings simultaneously will listen on both 0.0.0.0:{0}(IPv4) and :::{0}(IPv6)',
addressHelper: 'Specify the IP address on which the SSH service will listen',
permitRootLogin: 'root user',
rootSettingHelper: 'The default login mode is SSH for user root.',
rootHelper1: 'Allow SSH login',

View File

@ -1066,7 +1066,9 @@ const message = {
port: '連接端口',
portHelper: '指定 SSH 服務監聽的端口號默認為 22',
listenAddress: '監聽地址',
addressHelper: '指定 SSH 服務監聽的 IP 地址默認為 0.0.0.0即所有的網絡接口都會被監聽',
allV4V6: '0.0.0.0:{0}(IPv4) :::{0}(IPv6)',
listenHelper: '同時取消 IPv4 IPv6 設置將會同時監聽 0.0.0.0:{0}(IPv4) :::{0}(IPv6)',
addressHelper: '指定 SSH 服務監聽的 IP 地址',
permitRootLogin: 'root 用戶',
rootSettingHelper: 'root 用戶 SSH 登錄方式默認所有 SSH 登錄',
rootHelper1: '允許 SSH 登錄',

View File

@ -1067,7 +1067,9 @@ const message = {
port: '连接端口',
portHelper: '指定 SSH 服务监听的端口号默认为 22',
listenAddress: '监听地址',
addressHelper: '指定 SSH 服务监听的 IP 地址默认为 0.0.0.0即所有的网络接口都会被监听',
allV4V6: '0.0.0.0:{0}(IPv4) :::{0}(IPv6)',
listenHelper: '同时取消 IPv4 IPv6 设置将会同时监听 0.0.0.0:{0}(IPv4) :::{0}(IPv6)',
addressHelper: '指定 SSH 服务监听的 IP 地址',
permitRootLogin: 'root 用户',
rootSettingHelper: 'root 用户 SSH 登录方式默认所有 SSH 登录',
rootHelper1: '允许 SSH 登录',

View File

@ -150,7 +150,7 @@ import { ElForm } from 'element-plus';
import { createNetwork } from '@/api/modules/container';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { checkIpV6 } from '@/utils/util';
import { checkIp, checkIpV6 } from '@/utils/util';
const loading = ref(false);
@ -214,9 +214,7 @@ function checkGateway(rule: any, value: any, callback: any) {
if (value === '') {
callback();
}
const reg =
/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
if (!reg.test(value) && value !== '') {
if (checkIp(value)) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
@ -225,27 +223,11 @@ function checkGateway(rule: any, value: any, callback: any) {
function checkGatewayV6(rule: any, value: any, callback: any) {
if (value === '') {
callback();
} else {
const IPv4SegmentFormat = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])';
const IPv4AddressFormat = `(${IPv4SegmentFormat}[.]){3}${IPv4SegmentFormat}`;
const IPv6SegmentFormat = '(?:[0-9a-fA-F]{1,4})';
const IPv6AddressRegExp = new RegExp(
'^(' +
`(?:${IPv6SegmentFormat}:){7}(?:${IPv6SegmentFormat}|:)|` +
`(?:${IPv6SegmentFormat}:){6}(?:${IPv4AddressFormat}|:${IPv6SegmentFormat}|:)|` +
`(?:${IPv6SegmentFormat}:){5}(?::${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,2}|:)|` +
`(?:${IPv6SegmentFormat}:){4}(?:(:${IPv6SegmentFormat}){0,1}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,3}|:)|` +
`(?:${IPv6SegmentFormat}:){3}(?:(:${IPv6SegmentFormat}){0,2}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,4}|:)|` +
`(?:${IPv6SegmentFormat}:){2}(?:(:${IPv6SegmentFormat}){0,3}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,5}|:)|` +
`(?:${IPv6SegmentFormat}:){1}(?:(:${IPv6SegmentFormat}){0,4}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,6}|:)|` +
`(?::((?::${IPv6SegmentFormat}){0,5}:${IPv4AddressFormat}|(?::${IPv6SegmentFormat}){1,7}|:))` +
')(%[0-9a-zA-Z-.:]{1,})?$',
);
if (!IPv6AddressRegExp.test(value) && value !== '') {
}
if (checkIpV6(value)) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
}
}
function checkCidr(rule: any, value: any, callback: any) {

View File

@ -10,15 +10,40 @@
<template #header>
<DrawerHeader :header="$t('ssh.listenAddress')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-form
ref="formRef"
label-position="top"
:rules="rules"
:model="form"
@submit.prevent
v-loading="loading"
>
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item
:label="$t('ssh.listenAddress')"
prop="listenAddress"
:rules="Rules.ipV4V6OrDomain"
<el-alert class="common-prompt" :closable="false" type="error">
<template #default>
<span>
{{ $t('ssh.listenHelper', [form.port]) }}
</span>
</template>
</el-alert>
<el-form-item label="IPv4" prop="listenAddressV4">
<el-checkbox
v-model="form.ipv4All"
@change="form.listenAddressV4 = form.ipv4All ? '0.0.0.0' : form.listenAddressV4"
>
<el-input clearable v-model="form.listenAddress" />
{{ $t('setting.bindAll') }}
</el-checkbox>
<el-input :disabled="form.ipv4All" clearable v-model="form.listenAddressV4"></el-input>
</el-form-item>
<el-form-item label="IPv6" prop="listenAddressV6">
<el-checkbox
v-model="form.ipv6All"
@change="form.listenAddressV6 = form.ipv6All ? '::' : form.listenAddressV6"
>
{{ $t('setting.bindAll') }}
</el-checkbox>
<el-input :disabled="form.ipv6All" clearable v-model="form.listenAddressV6"></el-input>
</el-form-item>
</el-col>
</el-row>
@ -39,26 +64,66 @@ import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { ElMessageBox, FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { updateSSH } from '@/api/modules/host';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { checkIp, checkIpV6 } from '@/utils/util';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
port: number;
address: string;
}
const drawerVisible = ref();
const loading = ref();
const form = reactive({
listenAddress: '0.0.0.0',
port: 22,
ipv4All: false,
ipv6All: false,
listenAddressV4: '',
listenAddressV6: '',
});
const rules = reactive({
listenAddressV4: [{ validator: checkIPv4, trigger: 'blur' }],
listenAddressV6: [{ validator: checkIPv6, trigger: 'blur' }],
});
function checkIPv4(rule: any, value: any, callback: any) {
if (value === '') {
callback();
}
if (checkIp(value)) {
return callback(new Error(i18n.global.t('commons.rule.ip')));
}
callback();
}
function checkIPv6(rule: any, value: any, callback: any) {
if (value === '') {
callback();
}
if (checkIpV6(value)) {
return callback(new Error(i18n.global.t('commons.rule.ip')));
}
callback();
}
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.listenAddress = params.address;
let items = params.address.split(',');
for (const item of items) {
if (item.indexOf(':') !== -1) {
form.listenAddressV6 = item;
form.ipv6All = item === '::';
continue;
}
form.listenAddressV4 = item;
form.ipv4All = item === '0.0.0.0';
}
form.port = params.port;
drawerVisible.value = true;
};
@ -66,8 +131,19 @@ const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
let itemAddr = [];
if (form.listenAddressV4 !== '') {
itemAddr.push(form.listenAddressV4);
}
if (form.listenAddressV6 !== '') {
itemAddr.push(form.listenAddressV6);
}
let addr =
itemAddr.join(',') === '' || itemAddr.join(',') === '0.0.0.0,::'
? i18n.global.t('ssh.allV4V6', [form.port])
: itemAddr.join(',');
ElMessageBox.confirm(
i18n.global.t('ssh.sshChangeHelper', [i18n.global.t('ssh.listenAddress'), form.listenAddress]),
i18n.global.t('ssh.sshChangeHelper', [i18n.global.t('ssh.listenAddress'), addr]),
i18n.global.t('ssh.sshChange'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
@ -79,7 +155,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
let params = {
key: 'ListenAddress',
oldValue: '',
newValue: form.listenAddress,
newValue: itemAddr.join(','),
};
loading.value = true;
await updateSSH(params)

View File

@ -71,7 +71,7 @@
<span class="input-help">{{ $t('ssh.portHelper') }}</span>
</el-form-item>
<el-form-item :label="$t('ssh.listenAddress')" prop="listenAddress">
<el-input disabled v-model="form.listenAddress">
<el-input disabled v-model="form.listenAddressItem">
<template #append>
<el-button @click="onChangeAddress" icon="Setting">
{{ $t('commons.button.set') }}
@ -184,6 +184,7 @@ const form = reactive({
message: '',
port: 22,
listenAddress: '',
listenAddressItem: '',
passwordAuthentication: 'yes',
pubkeyAuthentication: 'yes',
encryptionMode: '',
@ -222,7 +223,7 @@ const onChangeRoot = () => {
rootsRef.value.acceptParams({ permitRootLogin: form.permitRootLogin });
};
const onChangeAddress = () => {
addressRef.value.acceptParams({ address: form.listenAddress });
addressRef.value.acceptParams({ address: form.listenAddress, port: form.port });
};
const onOperate = async (operation: string) => {
@ -325,6 +326,10 @@ const search = async () => {
form.port = Number(res.data.port);
autoStart.value = res.data.autoStart ? 'enable' : 'disable';
form.listenAddress = res.data.listenAddress;
form.listenAddressItem =
form.listenAddress === '' || form.listenAddress === '0.0.0.0,::'
? i18n.global.t('ssh.allV4V6', [form.port])
: form.listenAddress;
form.passwordAuthentication = res.data.passwordAuthentication;
form.pubkeyAuthentication = res.data.pubkeyAuthentication;
form.permitRootLogin = res.data.permitRootLogin;