From 2324494dd70f1c70a4d61107cfe46c552d133bd8 Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:45:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E6=9C=BA=E5=99=A8?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/lxc/lxc.go | 33 ++++++++++++++++++++++++++------ backend/internal/lxc/lxc_test.go | 17 ++++++++++++++++ frontend/src/utils/i18n.ts | 1 + 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/backend/internal/lxc/lxc.go b/backend/internal/lxc/lxc.go index 8ea8d01..12c7255 100644 --- a/backend/internal/lxc/lxc.go +++ b/backend/internal/lxc/lxc.go @@ -545,9 +545,11 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error { fmt.Printf("Warning: failed to install IPv6 init in %s: %v\n", lxcName, err) } } - cfg.ReportProgress("ssh", "安装并配置 SSH 服务") - if err := m.preconfigureSSH(rootfsPath, cfg.TemplateID, sshAccess.Mode); err != nil { + cfg.ReportProgress("ssh", "检测并预配置 SSH 服务") + if configured, err := m.preconfigureSSHIfInstalled(rootfsPath, cfg.TemplateID, sshAccess.Mode); err != nil { fmt.Printf("Warning: failed to pre-configure SSH in %s: %v\n", lxcName, err) + } else if !configured { + fmt.Printf("SSH server is not bundled in %s; installation deferred until after first boot\n", lxcName) } if sshAccess.PublicKey != "" { if err := m.installRootAuthorizedKey(rootfsPath, sshAccess.PublicKey); err != nil { @@ -898,6 +900,25 @@ func (m *Manager) preconfigureSSH(rootfsPath, templateID string, sshAuthMode str return nil } +// preconfigureSSHIfInstalled keeps image creation independent from external +// package mirrors. Minimal images install SSH asynchronously after first boot. +func (m *Manager) preconfigureSSHIfInstalled(rootfsPath, templateID, sshAuthMode string) (bool, error) { + if !rootfsHasSSHD(rootfsPath) { + return false, nil + } + return true, m.preconfigureSSH(rootfsPath, templateID, sshAuthMode) +} + +func rootfsHasSSHD(rootfsPath string) bool { + for _, relativePath := range []string{"usr/sbin/sshd", "sbin/sshd", "usr/bin/sshd"} { + info, err := os.Stat(filepath.Join(rootfsPath, relativePath)) + if err == nil && !info.IsDir() { + return true + } + } + return false +} + // applyResourceLimits applies cgroup v2 limits and mandatory security hardening to container config. func (m *Manager) applyResourceLimits(lxcName string, cfg ContainerConfig) error { cfg.NormalizeResourceAliases() @@ -1877,9 +1898,7 @@ func (m *Manager) StartContainer(id int) error { if err := m.ensureLANHostAccess(c); err != nil { fmt.Printf("Warning: failed to prepare LAN IPv4 host access for %s: %v\n", lxcName, err) } - if err := m.EnsureSSH(id); err != nil { - return err - } + m.WarmSSHAsync(id, "container start") } if current := config.FindContainer(id); current != nil { @@ -3336,8 +3355,10 @@ func (m *Manager) ReinstallContainer(id int, templateID string, authConfig ...Co } } c.SSHPassword = sshAccess.Password - if err := m.preconfigureSSH(rootfsPath, templateID, sshAccess.Mode); err != nil { + if configured, err := m.preconfigureSSHIfInstalled(rootfsPath, templateID, sshAccess.Mode); err != nil { fmt.Printf("Warning: failed to pre-configure SSH in %s after reinstall: %v\n", lxcName, err) + } else if !configured { + fmt.Printf("SSH server is not bundled in %s; installation deferred until after reinstall boot\n", lxcName) } if sshAccess.PublicKey != "" { if err := m.installRootAuthorizedKey(rootfsPath, sshAccess.PublicKey); err != nil { diff --git a/backend/internal/lxc/lxc_test.go b/backend/internal/lxc/lxc_test.go index ee2eb61..b31e4d0 100644 --- a/backend/internal/lxc/lxc_test.go +++ b/backend/internal/lxc/lxc_test.go @@ -121,6 +121,23 @@ func TestManagedPrlimitLinesDoNotSetNproc(t *testing.T) { } } +func TestRootfsHasSSHD(t *testing.T) { + rootfs := t.TempDir() + if rootfsHasSSHD(rootfs) { + t.Fatal("empty rootfs unexpectedly reports sshd") + } + sshd := filepath.Join(rootfs, "usr", "sbin", "sshd") + if err := os.MkdirAll(filepath.Dir(sshd), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(sshd, []byte("#!/bin/sh\n"), 0755); err != nil { + t.Fatal(err) + } + if !rootfsHasSSHD(rootfs) { + t.Fatal("executable sshd was not detected") + } +} + func TestSameFilesystemPathResolvesContainerStorageSymlink(t *testing.T) { base := t.TempDir() storageContainer := filepath.Join(base, "storage", "ct-1") diff --git a/frontend/src/utils/i18n.ts b/frontend/src/utils/i18n.ts index ebb7ccc..c10f4a1 100644 --- a/frontend/src/utils/i18n.ts +++ b/frontend/src/utils/i18n.ts @@ -795,6 +795,7 @@ const exact: Record = { '保存容器配置': 'Saving container configuration', '写入容器网络配置': 'Writing container network configuration', '安装并配置 SSH 服务': 'Installing and configuring SSH', + '检测并预配置 SSH 服务': 'Detecting and preconfiguring SSH', '转换非特权容器文件权限': 'Converting unprivileged container permissions', '设置容器登录凭据': 'Setting container login credentials', '启动容器并等待网络就绪': 'Starting container and waiting for network',