起因:
因项目需求,需要编写一个agent, 需支持Linux和Windows操作系统。 Agent里面有一个功能需要获取到服务器上所有已经被占用的端口。
实现方式:针对不同的操作系统,实现方式有所不同
netstat
指令,然后使用 os/exec
库来调用 shell脚本实现syscall.SysProcAttr
和syscall.LoadDLL
, 而这两个方法是windows系统下的专用库。问题: 这里会出出现一个问题,虽然程序在编译的时候可以通过GOOS
来区分编译到指定的操作系统的二进制包, 但是在编译过程中,编译器会进行代码检查,也会加载windows的代码逻辑。
初始代码如下:
// get address
func getAddress(addr string) string {
var address string
if strings.Contains(addr, "tcp") {
address = strings.TrimRight(addr, "tcp")
} else {
address = strings.TrimRight(addr, "udp")
}
return address
}
// CollectServerUsedPorts, collect all of the ports that have been used
func CollectServerUsedPorts(platform string) string {
var (
platformLower = strings.ToLower(platform)
cmd *exec.Cmd
err error
cmdOutPut []byte
)
if platformLower == "linux" {
// 执行 shell 指令, 获取tcp协议占用的端口
getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
cmd = exec.Command("bash", "-c", getUsedPortsCmd)
} else if platformLower == "windows" {
// 执行 powershell指令获取已经占用的端口号
getUsedPortsCmd := SelectScriptByWindowsVersion()
cmd = exec.Command("powershell", "-Command", getUsedPortsCmd)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
} else {
cmd = nil
}
if cmd != nil {
if cmdOutPut, err = cmd.Output(); err != nil {
log.Errorf("err to execute command %s, %s", cmd.String(), err.Error())
return ""
}
return strings.Trim(string(cmdOutPut), "\n")
}
return ""
}
func SelectScriptByWindowsVersion() string {
var getUsedPortsCmd string
version, err := getWindowsVersion()
if err != nil {
log.Errorf("无法获取Windows版本信息: %s", err.Error())
return ""
}
// if system version is lower than windows 8
if version < 6.2 {
log.Warnf("Windows 版本低于 Windows 8")
getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
} else {
getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
}
return getUsedPortsCmd
}
func getWindowsVersion() (float64, error) {
mod, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
return 0, err
}
defer func() {
_ = mod.Release()
}()
proc, err := mod.FindProc("GetVersion")
if err != nil {
return 0, err
}
version, _, _ := proc.Call()
majorVersion := byte(version)
minorVersion := byte(version >> 8)
return float64(majorVersion) + float64(minorVersion)/10, nil
}
# 编译linux二进制文件
go build -ldflags "-linkmode external -extldflags '-static'" -tags musl -o main main.go
# 错误输出如下
windows.go:31:22: undefined: syscall.LoadDLL
windows.go:56:41: unknown field 'HideWindow' in struct literal of type syscall.SysProcAttr
# 错误原因
- 内置库syscall,在linux编译时,其syscall.SysProcAttr 结构体并没有`HideWindow`字段;
- linux 下也没有 syscall.LoadDLL方法
- 编译和代码执行逻辑不一样,虽然代码有检查系统服务器类型的逻辑,但是编译时需要加载代码中的每一行代码逻辑,
- 将其编译成汇编,然后再交给计算机执行,所以会出现编译错误
Go语言在编译时除了有对整个项目编译的 参数控制 , 如 参数GOOS=windows
表示编译成widnwos系统下的二进制文件。 但是这个参数只能控制项目级别的, 对于上面这种情况,需要控制文件级别的编译, 当然 Go也是支持的,在提取出 windows 逻辑的代码为独立文件,在文件开头使用 // + build windows
语法。修改如下:
used_ports/windows.go
如下: // +build windows
func SelectScriptByWindowsVersion() string {
var getUsedPortsCmd string
version, err := getWindowsVersion()
if err != nil {
log.Errorf("无法获取Windows版本信息: %s", err.Error())
return ""
}
// if system version is lower than windows 8
if version < 6.2 {
log.Warnf("Windows 版本低于 Windows 8")
getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
} else {
getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
}
return getUsedPortsCmd
}
func getWindowsVersion() (float64, error) {
mod, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
return 0, err
}
defer func() {
_ = mod.Release()
}()
proc, err := mod.FindProc("GetVersion")
if err != nil {
return 0, err
}
version, _, _ := proc.Call()
majorVersion := byte(version)
minorVersion := byte(version >> 8)
return float64(majorVersion) + float64(minorVersion)/10, nil
}
将原来逻辑改成如下:
windows.go
// CollectServerUsedPorts, collect all of the ports that have been used
func CollectServerUsedPorts(platform string) string {
var (
platformLower = strings.ToLower(platform)
cmd *exec.Cmd
err error
cmdOutPut []byte
)
if platformLower == "linux" {
// 执行 shell 指令, 获取tcp协议占用的端口
getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
cmd = exec.Command("bash", "-c", getUsedPortsCmd)
} else if platformLower == "windows" {
// todo: 需要优化,通过接口映射避免编译的问题
// Linux 编译时需要隐藏下面代码,
getUsedPortsCmd := used_ports.CollectWindowsUsedPorts()
cmd = exec.Command("powershell", "-Command", getUsedPortsCmd)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
} else {
cmd = nil
}
if cmd != nil {
if cmdOutPut, err = cmd.Output(); err != nil {
log.Errorf("err to execute command %s, %s", cmd.String(), err.Error())
return ""
}
return strings.Trim(string(cmdOutPut), "\n")
}
return ""
}
光修改了上面的逻辑也不行,因为编译的时候代码依然会执行,此时会报如下错误
tools.go:121:15: undefined: used_ports.CollectWindowsUsedPorts
# 这是因为虽然linux编译时,不会编译 windows.go的文件,同时会导致 模块下的
#CollectWindowsUsedPorts 方法不存在
最终修复方式如下
if
判断语法来做显示判断,这样可以避免编译时显示加载因操作系统带来的编译冲突init()
方式初始化 接口实现/* 实现接口目录如下
├── used_ports
│ ├── linux.go
│ ├── used_ports.go
│ └── windows.go
*/
package used_ports
import "os/exec"
type UsedPortCollector interface {
CollectHaveUsedPorts() *exec.Cmd
}
var UsedPortCollectorMap = make(map[string]UsedPortCollector, 2)
func Register(platformOS string, collector UsedPortCollector) {
if _, ok := Find(platformOS); ok {
return
}
UsedPortCollectorMap[platformOS] = collector
}
func Find(platformOS string) (UsedPortCollector, bool) {
c, ok := UsedPortCollectorMap[platformOS]
return c, ok
}
package used_ports
import (
"os/exec"
)
func init() {
Register("linux", newLinuxUsedPorts())
}
type linuxPortCollectorImpl struct{}
func (w linuxPortCollectorImpl) CollectHaveUsedPorts() *exec.Cmd {
// 执行 shell 指令, 获取tcp协议占用的端口
getUsedPortsCmd := `netstat -tln | awk '{print $4}' | awk -F: '{print $NF}' | egrep -o '[0-9]+' | sort -n | uniq | paste -s -d ","`
cmd := exec.Command("bash", "-c", getUsedPortsCmd)
return cmd
}
// newLinuxUsedPorts 返回Windows系统下的端口收集器实例
func newLinuxUsedPorts() UsedPortCollector {
return linuxPortCollectorImpl{}
}
// +build windows
package used_ports
import (
"os/exec"
"syscall"
)
func init() {
Register("windows", newWindowsCollector())
}
// 结构体
type windowsPortCollectorImpl struct{}
// 实现接口方法
func (w windowsPortCollectorImpl) CollectHaveUsedPorts() *exec.Cmd {
// 执行 powershell指令获取已经占用的端口号
getUsedPortsCmd := selectScriptByWindowsVersion()
cmd := exec.Command("powershell", "-Command", getUsedPortsCmd)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
return cmd
}
// newWindowsCollector 返回Windows系统下的端口收集器实例,算是工厂方法
func newWindowsCollector() UsedPortCollector {
return windowsPortCollectorImpl{}
}
func selectScriptByWindowsVersion() string {
var getUsedPortsCmd string
version, err := getWindowsVersion()
if err != nil {
return ""
}
// if system version is lower than windows 8
if version < 6.2 {
getUsedPortsCmd = `(netstat -an | Select-String 'LISTENING' | ForEach-Object { $_ -replace '\s+', ' ' } | ForEach-Object { ($_ -split ' ')[2] } | Where-Object {$_ -match '\d'} | ForEach-Object {[int]($_ -split ':')[-1]} | Sort-Object | Get-Unique ) -join ",".Replace("r","")`
} else {
getUsedPortsCmd = `((Get-NetTCPConnection | Where-Object {$_.State -eq 'Listen'} | Select-Object -ExpandProperty LocalPort) | Sort-Object {[int]$_} | Get-Unique) -join ",".Replace("r","")`
}
return getUsedPortsCmd
}
func getWindowsVersion() (float64, error) {
mod, err := syscall.LoadDLL("kernel32.dll")
if err != nil {
return 0, err
}
defer func() {
_ = mod.Release()
}()
proc, err := mod.FindProc("GetVersion")
if err != nil {
return 0, err
}
version, _, _ := proc.Call()
majorVersion := byte(version)
minorVersion := byte(version >> 8)
return float64(majorVersion) + float64(minorVersion)/10, nil
}
tools.go
...
// CollectServerUsedPorts, collect all of the ports that have been used
func CollectServerUsedPorts(platform string) string {
var (
platformLower = strings.ToLower(platform)
cmd *exec.Cmd
err error
cmdOutPut []byte
)
// 策略方法,获取操作系统对应的实例(接口)
if portCollector, ok := used_ports.Find(platformLower); ok {
cmd = portCollector.CollectHaveUsedPorts()
}
if cmd != nil {
if cmdOutPut, err = cmd.Output(); err != nil {
log.Errorf("err to execute command %s, %s", cmd.String(), err.Error())
return ""
}
return strings.Trim(string(cmdOutPut), "\n")
}
return ""
}
GOOS=windows
来区分编译成对应操作系统的二进制文件// + build windows
进行控制init()
初始化方法的使用