1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-31 22:18:07 +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, Status: constant.StatusEnable,
Message: "", Message: "",
Port: "22", Port: "22",
ListenAddress: "0.0.0.0", ListenAddress: "",
PasswordAuthentication: "yes", PasswordAuthentication: "yes",
PubkeyAuthentication: "yes", PubkeyAuthentication: "yes",
PermitRootLogin: "yes", PermitRootLogin: "yes",
@ -90,7 +90,12 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
data.Port = strings.ReplaceAll(line, "Port ", "") data.Port = strings.ReplaceAll(line, "Port ", "")
} }
if strings.HasPrefix(line, "ListenAddress ") { 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 ") { if strings.HasPrefix(line, "PasswordAuthentication ") {
data.PasswordAuthentication = strings.ReplaceAll(line, "PasswordAuthentication ", "") data.PasswordAuthentication = strings.ReplaceAll(line, "PasswordAuthentication ", "")
@ -366,33 +371,34 @@ func sortFileList(fileNames []sshFileItem) []sshFileItem {
return fileNames return fileNames
} }
func updateSSHConf(oldFiles []string, param string, value interface{}) []string { func updateSSHConf(oldFiles []string, param string, value string) []string {
hasKey := false var valueItems []string
if param != "ListenAddress" {
valueItems = append(valueItems, value)
} else {
if value != "" {
valueItems = strings.Split(value, ",")
}
}
var newFiles []string var newFiles []string
for _, line := range oldFiles { for _, line := range oldFiles {
if strings.HasPrefix(line, param+" ") { lineItem := strings.TrimSpace(line)
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value)) if (strings.HasPrefix(lineItem, param) || strings.HasPrefix(lineItem, fmt.Sprintf("#%s", param))) && len(valueItems) != 0 {
hasKey = true 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 continue
} }
newFiles = append(newFiles, line) newFiles = append(newFiles, line)
} }
if !hasKey { if len(valueItems) != 0 {
newFiles = []string{} for _, item := range valueItems {
for _, line := range oldFiles { newFiles = append(newFiles, fmt.Sprintf("%s %s", param, item))
if strings.HasPrefix(line, fmt.Sprintf("#%s ", param)) && !hasKey {
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value))
hasKey = true
continue
}
newFiles = append(newFiles, line)
} }
} }
if !hasKey {
newFiles = []string{}
newFiles = append(newFiles, oldFiles...)
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value))
}
return newFiles 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?', '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.', portHelper: 'Specifies the port number monitored by the SSH service. The default port number is 22.',
listenAddress: 'Listening address', listenAddress: 'Listening address',
addressHelper: allV4V6: '0.0.0.0:{0}(IPv4) and :::{0}(IPv6)',
'Specify the IP address monitored by the SSH service. The default value is 0.0.0.0. That is, all network interfaces are monitored.', 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', permitRootLogin: 'root user',
rootSettingHelper: 'The default login mode is SSH for user root.', rootSettingHelper: 'The default login mode is SSH for user root.',
rootHelper1: 'Allow SSH login', rootHelper1: 'Allow SSH login',

View File

@ -1066,7 +1066,9 @@ const message = {
port: '連接端口', port: '連接端口',
portHelper: '指定 SSH 服務監聽的端口號默認為 22', portHelper: '指定 SSH 服務監聽的端口號默認為 22',
listenAddress: '監聽地址', 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 用戶', permitRootLogin: 'root 用戶',
rootSettingHelper: 'root 用戶 SSH 登錄方式默認所有 SSH 登錄', rootSettingHelper: 'root 用戶 SSH 登錄方式默認所有 SSH 登錄',
rootHelper1: '允許 SSH 登錄', rootHelper1: '允許 SSH 登錄',

View File

@ -1067,7 +1067,9 @@ const message = {
port: '连接端口', port: '连接端口',
portHelper: '指定 SSH 服务监听的端口号默认为 22', portHelper: '指定 SSH 服务监听的端口号默认为 22',
listenAddress: '监听地址', 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 用户', permitRootLogin: 'root 用户',
rootSettingHelper: 'root 用户 SSH 登录方式默认所有 SSH 登录', rootSettingHelper: 'root 用户 SSH 登录方式默认所有 SSH 登录',
rootHelper1: '允许 SSH 登录', rootHelper1: '允许 SSH 登录',

View File

@ -150,7 +150,7 @@ import { ElForm } from 'element-plus';
import { createNetwork } from '@/api/modules/container'; import { createNetwork } from '@/api/modules/container';
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { checkIpV6 } from '@/utils/util'; import { checkIp, checkIpV6 } from '@/utils/util';
const loading = ref(false); const loading = ref(false);
@ -214,9 +214,7 @@ function checkGateway(rule: any, value: any, callback: any) {
if (value === '') { if (value === '') {
callback(); callback();
} }
const reg = if (checkIp(value)) {
/^(\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 !== '') {
return callback(new Error(i18n.global.t('commons.rule.formatErr'))); return callback(new Error(i18n.global.t('commons.rule.formatErr')));
} }
callback(); callback();
@ -225,27 +223,11 @@ function checkGateway(rule: any, value: any, callback: any) {
function checkGatewayV6(rule: any, value: any, callback: any) { function checkGatewayV6(rule: any, value: any, callback: any) {
if (value === '') { if (value === '') {
callback(); 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 !== '') {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
} }
if (checkIpV6(value)) {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
} }
function checkCidr(rule: any, value: any, callback: any) { function checkCidr(rule: any, value: any, callback: any) {

View File

@ -10,15 +10,40 @@
<template #header> <template #header>
<DrawerHeader :header="$t('ssh.listenAddress')" :back="handleClose" /> <DrawerHeader :header="$t('ssh.listenAddress')" :back="handleClose" />
</template> </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-row type="flex" justify="center">
<el-col :span="22"> <el-col :span="22">
<el-form-item <el-alert class="common-prompt" :closable="false" type="error">
:label="$t('ssh.listenAddress')" <template #default>
prop="listenAddress" <span>
:rules="Rules.ipV4V6OrDomain" {{ $t('ssh.listenHelper', [form.port]) }}
> </span>
<el-input clearable v-model="form.listenAddress" /> </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"
>
{{ $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-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -39,26 +64,66 @@ import { reactive, ref } from 'vue';
import i18n from '@/lang'; import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { ElMessageBox, FormInstance } from 'element-plus'; import { ElMessageBox, FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { updateSSH } from '@/api/modules/host'; import { updateSSH } from '@/api/modules/host';
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
import { checkIp, checkIpV6 } from '@/utils/util';
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps { interface DialogProps {
port: number;
address: string; address: string;
} }
const drawerVisible = ref(); const drawerVisible = ref();
const loading = ref(); const loading = ref();
const form = reactive({ 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 formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => { 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; drawerVisible.value = true;
}; };
@ -66,8 +131,19 @@ const onSave = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
if (!valid) return; 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( 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'), i18n.global.t('ssh.sshChange'),
{ {
confirmButtonText: i18n.global.t('commons.button.confirm'), confirmButtonText: i18n.global.t('commons.button.confirm'),
@ -79,7 +155,7 @@ const onSave = async (formEl: FormInstance | undefined) => {
let params = { let params = {
key: 'ListenAddress', key: 'ListenAddress',
oldValue: '', oldValue: '',
newValue: form.listenAddress, newValue: itemAddr.join(','),
}; };
loading.value = true; loading.value = true;
await updateSSH(params) await updateSSH(params)

View File

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