menu Chancel's blog
rss_feed
Chancel's blog
有善始者实繁,能克终者盖寡。

Linux下使用ssh管理服务器

作者:Chancel Yang, 创建:2019-05-15, 字数:7998, 已阅:701, 最后更新:2024-03-10

在Windows下,有着许多优秀的SSH客户端,在Linux上,选择就没有那么广泛了

使用ssh连接服务器是每一个软件开发者常见的操作,无论是免费的MobaXterm还是付费的termius都算得上是非常好用,尤其是termius,作为商业工具兼顾了易用与美观

termius收费较高,免费的版本缺失不少功能,与免费的MobaXterm没有太大差别

后来发现直接使用ssh也非常好用,如在$HOME/.ssh/config添加如下配置

TEXT
Host company-develop
  HostName 192.168.122.4
  Port 22
  User chancel
  IdentityFile /home/chancel/.ssh/id_rsa

...

然后在命令行输入ssh+tab按键,可以看到智能补全

Bash
~❯ ssh company-
company-develop  ...

使用多了,发现这样也非常便捷

1. config

$HOME/.ssh/config文件是SSH客户端的配置文件,用于设置和管理SSH连接的参数和选项,它可以包含多个主机配置块,每个主机配置块定义了与特定主机的详细连接参数

配置文件中的每个主机配置块由以下几个常见部分组成:

  1. Host:指定主机的名称或模式匹配规则(例如,匹配通配符或正则表达式)
  2. HostName:指定要连接的主机的名称或IP地址。
  3. User:指定要使用的用户名。
  4. Port:指定连接的端口号。
  5. IdentityFile:指定用于身份验证的私钥文件路径。
  6. ForwardAgent:指定是否在连接中转发本地SSH代理。
  7. ProxyCommand:指定连接中使用的代理命令。

通过修改$HOME/.ssh/config文件,可以方便地设置和管理SSH连接的参数,不用每次连接时都手动输入命令行选项

此外,配置文件还可以提供更高级的功能,如通过别名快速连接主机、设置跳板主机、配置连接超时等,这些参数可以参考man ssh_config获得

1.1. 实践

编辑~/.ssh/config 文件

TEXT
Host chancel-pc # 别名,用于连接时驶入
    HostName 192.168.10.1 # 连接IP
    User ycs # 用户名
    Port 10086 # SSH 端口
    ServerAliveInterval 60 # 每60秒发送一次心跳避免SSH连接中断
    IdentityFile .ssh/id_rsa # 本地证书

然后授权

Bash
chmod 600 ~/.ssh/config

测试连接

Bash
ssh chancel-pc

2. go-ssh

2.1. 列表管理

如果config文件中配置的服务器多起来还是非常难管理的,这个时候图形界面的SSH客户端会有优势,能够列表展示所有服务器信息

这个时候我们可以利用一些第三方插件来实现如ssh-config-managersshmenusshrc

2.2. 代码实现

考虑到这个需求并不复杂,且ssh的端口、证书、密码属于敏感信息,可以自己实现一个脚本来展现列表形式

这里用golang来实现,借助golang优秀的打包机制,可以写一次代码在多个平台上使用

Go
package main

import (
    "bufio"
    "flag"
    "fmt"
    "os"
    "os/exec"
    "strings"
)

// Function to parse SSH configuration file and return a slice of maps containing the configuration details
func parseSSHConfig(configPath string) ([]map[string]string, error) {
    // Open the SSH configuration file
    configFile, err := os.Open(configPath)
    if err != nil {
    	fmt.Println(err)
    	return nil, err
    }
    defer configFile.Close()

    // Initialize variables to store the configuration data
    configDicts := []map[string]string{} // Slice of maps to store multiple configurations
    currentDict := map[string]string{} // Map to store the current configuration

    // Create a scanner to read the configuration file line by line
    scanner := bufio.NewScanner(configFile)
    for scanner.Scan() {
    	line := strings.TrimSpace(scanner.Text()) // Remove leading and trailing whitespaces

    	// Skip empty lines and lines starting with "#"
    	if line == "" || strings.HasPrefix(line, "#") {
    		continue
    	}

    	// If line starts with "Host ", it indicates a new configuration block
    	if strings.HasPrefix(line, "Host ") {
    		// If there is any existing configuration, add it to the configDicts slice
    		if len(currentDict) > 0 {
    			configDicts = append(configDicts, currentDict)
    		}

    		// Create a new map for the current configuration block
    		currentDict = map[string]string{}
    		currentDict["Host"] = strings.TrimSpace(strings.TrimPrefix(line, "Host "))
    	} else {
    		// Split the line into key-value pairs
    		parts := strings.SplitN(line, " ", 2)
    		if len(parts) == 2 {
    			key := strings.TrimSpace(parts[0])
    			value := strings.TrimSpace(parts[1])

    			// Store the key-value pair in the currentDict map
    			currentDict[key] = value
    		}
    	}
    }

    // Add the last configuration block to the configDicts slice
    if len(currentDict) > 0 {
    	configDicts = append(configDicts, currentDict)
    }

    // Return the slice of maps containing the configuration details and no error
    return configDicts, nil
}

// Function to print the SSH configuration
func printSSHConfig(configs []map[string]string) {
    // Printing header
    fmt.Println("************************ Hi, Welcome to use Go-SSH Tool *****************************")
    fmt.Println()
    fmt.Println("+-----+------------------------------+-------------------------+------------------------------------------+")
    fmt.Println("| id  | Host                         | username                | address                                  |")
    fmt.Println("+-----+------------------------------+-------------------------+------------------------------------------+")

    // Printing each SSH configuration
    for i, config := range configs {
    	fmt.Printf("| %-3d | %-28s | %-23s | %-40s |\n", i+1, config["Host"], config["User"], config["HostName"])
    }

    fmt.Println("+-----+------------------------------+-------------------------+------------------------------------------+")
    fmt.Println()
    fmt.Println("Tips: Press a number between 1 and", len(configs)-1, "to select the host to connect, or \"q\" to quit.")
    fmt.Println()
}

func main() {
    // Parsing command line arguments
    configPath := flag.String("c", "$HOME/.ssh/config", "Path to SSH config file")
    flag.Parse()

    // Expanding environment variables in the config path
    expandedPath := os.ExpandEnv(*configPath)

    // Parsing the SSH config file
    configs, err := parseSSHConfig(expandedPath)
    if err != nil {
    	fmt.Println("Error:", err)
    	return
    }

    // Printing the SSH configuration
    printSSHConfig(configs)

    // Creating a scanner to read user input
    scanner := bufio.NewScanner(os.Stdin)

    for {
    	fmt.Print("# ")
    	scanner.Scan()
    	input := scanner.Text()

    	// Exiting the program if user enters "q"
    	if input == "q" {
    		return
    	}

    	id := -1
    	// Parsing the user input to get the selected SSH configuration ID
    	fmt.Sscanf(input, "%d", &id)
    	if id >= 1 && id <= len(configs) {
    		// Getting the selected SSH configuration
    		sshConfig := configs[id-1]

    		// Creating arguments for the "ssh" command
    		var args []string
    		for key, value := range sshConfig {
    			// Skipping "Host", "User", and "HostName" keys
    			if key == "Host" || key == "User" || key == "HostName" {
    				continue
    			}
    			arg := fmt.Sprintf("-o %s=%s", key, value)
    			args = append(args, arg)
    		}

    		// Appending username and host name to the command arguments
    		cmdArgs := append(args, fmt.Sprintf("%s@%s", sshConfig["User"], sshConfig["HostName"]))

    		// Creating the "ssh" command
    		sshCmd := exec.Command("ssh", cmdArgs...)
    		sshCmd.Stdout = os.Stdout
    		sshCmd.Stdin = os.Stdin
    		sshCmd.Stderr = os.Stderr

    		// Running the "ssh" command
    		err := sshCmd.Run()
    		if err != nil {
    			fmt.Println("Error:", err)
    			os.Exit(0)
    		}
    	} else {
    		fmt.Println("Error: Invalid input")
    	}

    	// Printing the SSH configuration after each iteration
    	printSSHConfig(configs)
    }
}

代码仓库:github仓库 - chancelyg/go-ssh

上面的代码请自行审阅,这个工具可以实现$HOME/.ssh/config中的ssh服务器以列表的形式呈现出来

你可以使用一个数字选择要连接的主机,然后会自动使用SSH连接到该主机

2.3. 使用效果

如下

Bash
❯ ./go-ssh
************************ Hi, Welcome to use Go-SSH Tool *****************************

+-----+------------------------------+-------------------------+------------------------------------------+
| id  | Host                         | username                | address                                  |
+-----+------------------------------+-------------------------+------------------------------------------+
| 1   | 192.168.122.4                | chancel                 | 192.168.122.4                            |
| 2   | 192.168.4.15                 | chancel                 | 192.168.4.15                             |
| 3   | 192.168.11.2                 | chancel                 | 192.168.11.2                             |
| 4   | 192.168.11.3                 | chancel                 | 192.168.11.3                             |
| 5   | 192.168.11.12                | chancel                 | 192.168.11.12                            |
| 6   | 192.168.4.10                 | chancel                 | 192.168.4.10                             |
+-----+------------------------------+-------------------------+------------------------------------------+

Tips: Press a number between 1 and 5 to select the host to connect, or "q" to quit.

#

3. 尾声

灵感


[[replyMessage== null?"发表评论":"发表评论 @ " + replyMessage.m_author]]

account_circle
email
web_asset
textsms

评论列表([[messageResponse.total]])

还没有可以显示的留言...
[[messageItem.m_author]] [[messageItem.m_author]]
[[messageItem.create_time]]
[[getEnviron(messageItem.m_environ)]]
[[subMessage.m_author]] [[subMessage.m_author]] @ [[subMessage.parent_message.m_author]] [[subMessage.parent_message.m_author]]
[[subMessage.create_time]]
[[getEnviron(messageItem.m_environ)]]