这个article 涵盖了一个端口扫描器示例中的管道模式,推荐看看。
端口扫描器旨在探测服务器或主机的开放端口
上图显示了端口扫描器的整个管道。在下一节中,我们将逐个解释每个相关的功能。
- 函数初始化
- Func parsePortsToScan
- 结构扫描操作
- 功能基因
- 功能扫描
- 功能过滤器
- 功能商店
- 主函数
init 函数定义了用户传入的参数。 ports 变量是要扫描的端口字符串,用破折号分隔。 outFile 变量是写入结果的文件。
var ports string
var outFile string
func init() {
flag.StringVar(&ports, "ports", "80", "Port(s) (e.g. 80, 22-100).")
flag.StringVar(&outFile, "outfile", "scans.csv",
"Destination of scan results (defaults to scans.csv)")
}
主函数负责执行函数的管道。它从命令行参数中获取一片 int 端口和一个字符串输出文件。
func main() {
flag.Parse()
portsToScan, err := parsePortsToScan(ports)
if err != nil {
fmt.Printf("Failed to parse ports to scan: %s\n", err)
os.Exit(1)
}
dest, err := os.Create(outFile)
if err != nil {
fmt.Printf("Failed to create scan results destination: %s\n", err)
os.Exit(2)
}
// pipeline
// scanChan := store(dest, filter(scan(gen(portsToScan...))))
// broken up for explainability
var scanChan <-chan scanOp
scanChan = gen(portsToScan...)
scanChan = scan(scanChan)
scanChan = filter(scanChan)
scanChan = store(dest, scanChan)
for s := range scanChan {
if !s.open && s.scanErr != fmt.Sprintf("dial tcp 127.0.0.1:%d: connect: connection refused", s.port) {
fmt.Println(s.scanErr)
}
}
}
parsePortsToScan 函数从命令行参数解析要扫描的端口。如果参数无效,则返回错误。如果参数有效,则返回一个整数切片。
func parsePortsToScan(portsFlag string) ([]int, error) {
p, err := strconv.Atoi(portsFlag)
if err == nil {
return []int{p}, nil
}
ports := strings.Split(portsFlag, "-")
if len(ports) != 2 {
return nil, errors.New("unable to determine port(s) to scan")
}
minPort, err := strconv.Atoi(ports[0])
if err != nil {
return nil, fmt.Errorf("failed to convert %s to a valid port number", ports[0])
}
maxPort, err := strconv.Atoi(ports[1])
if err != nil {
return nil, fmt.Errorf("failed to convert %s to a valid port number", ports[1])
}
if minPort <= 0 || maxPort <= 0 {
return nil, fmt.Errorf("port numbers must be greater than 0")
}
var results []int
for p := minPort; p <= maxPort; p++ {
results = append(results, p)
}
return results, nil
}
scanOp 表示单端口扫描操作及其结果(open、scanErr、scanDuration)。 open 是一个布尔值,指示端口是否打开。如果扫描失败,scanErr 是一条错误消息。scanDuration 是执行扫描所花费的时间。
要将结果输出到 CSV 文件,CSV 编写器使用两种方法。 csvHeaders 返回字符串切片中的标题。 asSlice 将 scanOp 的值字段作为字符串切片返回。
type scanOp struct {
port int
open bool
scanErr string
scanDuration time.Duration
}
func (so scanOp) csvHeaders() []string {
return []string{"port", "open", "scanError", "scanDuration"}
}
func (so scanOp) asSlice() []string {
return []string{
strconv.FormatInt(int64(so.port), 10),
strconv.FormatBool(so.open),
so.scanErr,
so.scanDuration.String(),
}
}
gen 函数是一个生成器函数,它从 int 端口切片返回一个 scanOps 结构值的缓冲通道。它用于创建将按顺序执行的函数管道,它是管道中的第一个函数。
func gen(ports ...int) <-chan scanOp {
out := make(chan scanOp, len(ports))
go func() {
defer close(out)
for _, p := range ports {
out <- scanOp{port: p}
}
}()
return out
}
扫描函数负责执行实际的端口扫描。它接受一个有缓冲的 scanOps 通道,并返回一个无缓冲的 scanOps 通道。
func scan(in <-chan scanOp) <-chan scanOp {
out := make(chan scanOp)
go func() {
defer close(out)
for scan := range in {
address := fmt.Sprintf("127.0.0.1:%d", scan.port)
start := time.Now()
conn, err := net.Dial("tcp", address)
scan.scanDuration = time.Since(start)
if err != nil {
scan.scanErr = err.Error()
} else {
conn.Close()
scan.open = true
}
out <- scan
}
}()
return out
}
filter函数负责过滤打开的scanOps。
func filter(in <-chan scanOp) <-chan scanOp {
out := make(chan scanOp)
go func() {
defer close(out)
for scan := range in {
if scan.open {
out <- scan
}
}
}()
return out
}
store 函数负责将 scanOps 存储在 CSV 文件中。它是管道中的最后一个函数。
func store(file io.Writer, in <-chan scanOp) <-chan scanOp {
csvWriter := csv.NewWriter(file)
out := make(chan scanOp)
go func() {
defer csvWriter.Flush()
defer close(out)
var headerWritten bool
for scan := range in {
if !headerWritten {
headers := scan.csvHeaders()
if err := csvWriter.Write(headers); err != nil {
fmt.Println(err)
break
}
headerWritten = true
}
values := scan.asSlice()
if err := csvWriter.Write(values); err != nil {
fmt.Println(err)
break
}
}
}()
return out
}
通道可用于将 goroutine 连接在一起,以便一个的输出是另一个的输入。当您的管道中有许多功能并想要连接它们时,它非常有用。