如何用Go语言通过syscall实现TCP SYN端口扫描

来源:站长查询作者:孙悟空头衔:草根站长
导读:本期聚焦于小伙伴创作的《如何用Go语言通过syscall实现TCP SYN端口扫描》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《如何用Go语言通过syscall实现TCP SYN端口扫描》有用,将其分享出去将是对创作者最好的鼓励。

TCP SYN端口扫描的核心逻辑是向目标端口发送SYN请求包,根据返回的响应判断端口状态:如果收到SYN+ACK响应说明端口开放,收到RST响应说明端口关闭,没有响应则可能是端口被过滤。Go语言标准库的net包封装了高层网络操作,要实现SYN扫描需要借助syscall包调用系统底层能力操作原始套接字。

如何用Go语言通过syscall实现TCP SYN端口扫描

核心原理与前置知识

SYN扫描属于半开扫描,不需要完成完整的三次握手流程,因此扫描速度更快,也更容易绕过部分简单的防火墙规则。实现该功能需要掌握三个核心知识点:

  • 原始套接字的使用:需要创建可以自定义IP包和TCP包内容的套接字,普通TCP套接字无法修改协议头字段
  • TCP协议头结构:需要手动构造包含SYN标志位的TCP头部,正确计算校验和
  • ICMP和TCP响应解析:需要监听网络响应,区分不同类型的返回包判断端口状态

基础环境准备

使用syscall操作原始套接字需要较高的系统权限,Linux系统下需要以root用户运行程序,Windows系统需要开启管理员权限。同时需要引入必要的包:

package main

import (
    "encoding/binary"
    "fmt"
    "syscall"
    "time"
)

原始套接字创建

在Linux系统下,我们可以通过syscall.Socket创建原始套接字,指定协议族为AF_INET,套接字类型为SOCK_RAW,协议为IPPROTO_RAW,这样我们就可以完全自定义IP层和TCP层的内容:

// 创建原始套接字
func createRawSocket() (int, error) {
    // AF_INET 表示IPv4,SOCK_RAW 表示原始套接字,IPPROTO_RAW 表示我们自行构造IP头
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
    if err != nil {
        return -1, fmt.Errorf("创建套接字失败: %v", err)
    }
    // 设置IP_HDRINCL选项,告诉内核我们会在发送的数据中包含IP头部
    err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
    if err != nil {
        syscall.Close(fd)
        return -1, fmt.Errorf("设置套接字选项失败: %v", err)
    }
    return fd, nil
}

TCP SYN包构造

构造TCP SYN包需要分别构造IP头部和TCP头部,其中TCP头部的SYN标志位需要置为1,同时要计算正确的TCP校验和。首先定义相关的结构体:

// IPv4头部结构,共20字节
type ipHeader struct {
    VersionIHL  uint8  // 版本号(4位) + 头部长度(4位)
    TOS         uint8  // 服务类型
    TotalLen    uint16 // 总长度
    ID          uint16 // 标识
    FlagsOffset uint16 // 标志(3位) + 分片偏移(13位)
    TTL         uint8  // 生存时间
    Protocol    uint8  // 协议类型,TCP为6
    Checksum    uint16 // IP头部校验和
    SrcIP       uint32 // 源IP地址
    DstIP       uint32 // 目标IP地址
}

// TCP头部结构,无选项时共20字节
type tcpHeader struct {
    SrcPort    uint16 // 源端口
    DstPort    uint16 // 目标端口
    SeqNum     uint32 // 序列号
    AckNum     uint32 // 确认号
    Offset     uint8  // 数据偏移(4位) + 保留位(4位)
    Flags      uint8  // 标志位,SYN为00000010即2
    Window     uint16 // 窗口大小
    Checksum   uint16 // TCP校验和
    UrgentPtr  uint16 // 紧急指针
}

接下来实现TCP校验和计算函数,校验和需要包含伪头部、TCP头部和TCP数据部分:

// 计算TCP校验和
func calculateTCPChecksum(srcIP, dstIP uint32, tcpHdr []byte) uint16 {
    // 构造伪头部:源IP(4字节) + 目标IP(4字节) + 保留(1字节) + 协议(1字节) + TCP长度(2字节)
    pseudoHeader := make([]byte, 12)
    binary.BigEndian.PutUint32(pseudoHeader[0:4], srcIP)
    binary.BigEndian.PutUint32(pseudoHeader[4:8], dstIP)
    pseudoHeader[8] = 0
    pseudoHeader[9] = 6 // TCP协议号
    binary.BigEndian.PutUint16(pseudoHeader[10:12], uint16(len(tcpHdr)))

    // 拼接伪头部和TCP头部
    sumBuf := append(pseudoHeader, tcpHdr...)

    // 计算16位累加和
    var sum uint32
    for i := 0; i < len(sumBuf); i += 2 {
        if i+1 < len(sumBuf) {
            sum += uint32(binary.BigEndian.Uint16(sumBuf[i : i+2]))
        } else {
            // 奇数长度补0
            sum += uint32(sumBuf[i]) << 8
        }
    }

    // 进位折叠
    for sum > 0xffff {
        sum = (sum & 0xffff) + (sum >> 16)
    }
    return uint16(^sum)
}

然后实现构造完整SYN包的函数:

// 构造TCP SYN包
func buildSYNPacket(srcIP, dstIP string, srcPort, dstPort uint16) ([]byte, error) {
    // 将IP字符串转为uint32
    var srcIPUint, dstIPUint uint32
    binary.Read(bytes.NewBuffer(net.ParseIP(srcIP).To4()), binary.BigEndian, &srcIPUint)
    binary.Read(bytes.NewBuffer(net.ParseIP(dstIP).To4()), binary.BigEndian, &dstIPUint)

    // 构造IP头部
    ipHdr := ipHeader{
        VersionIHL:  0x45, // IPv4,头部长度20字节(5*4)
        TOS:         0,
        TotalLen:    uint16(20 + 20), // IP头20字节 + TCP头20字节
        ID:          uint16(time.Now().UnixNano() & 0xffff),
        FlagsOffset: 0x4000, // 不分片
        TTL:         64,
        Protocol:    6, // TCP
        SrcIP:       srcIPUint,
        DstIP:       dstIPUint,
    }
    // IP校验和计算
    ipHdrBytes := make([]byte, 20)
    binary.BigEndian.PutUint16(ipHdrBytes[0:2], uint16(ipHdr.VersionIHL)<<8|uint16(ipHdr.TOS))
    binary.BigEndian.PutUint16(ipHdrBytes[2:4], ipHdr.TotalLen)
    binary.BigEndian.PutUint16(ipHdrBytes[4:6], ipHdr.ID)
    binary.BigEndian.PutUint16(ipHdrBytes[6:8], ipHdr.FlagsOffset)
    ipHdrBytes[8] = ipHdr.TTL
    ipHdrBytes[9] = ipHdr.Protocol
    binary.BigEndian.PutUint32(ipHdrBytes[12:16], ipHdr.SrcIP)
    binary.BigEndian.PutUint32(ipHdrBytes[16:20], ipHdr.DstIP)
    // 计算IP校验和
    var ipSum uint32
    for i := 0; i < 20; i += 2 {
        ipSum += uint32(binary.BigEndian.Uint16(ipHdrBytes[i : i+2]))
    }
    for ipSum > 0xffff {
        ipSum = (ipSum & 0xffff) + (ipSum >> 16)
    }
    ipHdr.Checksum = uint16(^ipSum)
    binary.BigEndian.PutUint16(ipHdrBytes[10:12], ipHdr.Checksum)

    // 构造TCP头部
    tcpHdr := tcpHeader{
        SrcPort:   srcPort,
        DstPort:   dstPort,
        SeqNum:    uint32(time.Now().UnixNano() & 0xffffffff),
        AckNum:    0,
        Offset:    0x50, // 数据偏移20字节(5*4),无选项
        Flags:     0x02, // SYN标志位置1
        Window:    65535,
        Checksum:  0, // 先置0,后续计算
        UrgentPtr: 0,
    }
    tcpHdrBytes := make([]byte, 20)
    binary.BigEndian.PutUint16(tcpHdrBytes[0:2], tcpHdr.SrcPort)
    binary.BigEndian.PutUint16(tcpHdrBytes[2:4], tcpHdr.DstPort)
    binary.BigEndian.PutUint32(tcpHdrBytes[4:8], tcpHdr.SeqNum)
    binary.BigEndian.PutUint32(tcpHdrBytes[8:12], tcpHdr.AckNum)
    tcpHdrBytes[12] = tcpHdr.Offset
    tcpHdrBytes[13] = tcpHdr.Flags
    binary.BigEndian.PutUint16(tcpHdrBytes[14:16], tcpHdr.Window)
    binary.BigEndian.PutUint16(tcpHdrBytes[18:20], tcpHdr.UrgentPtr)

    // 计算TCP校验和
    tcpChecksum := calculateTCPChecksum(srcIPUint, dstIPUint, tcpHdrBytes)
    binary.BigEndian.PutUint16(tcpHdrBytes[16:18], tcpChecksum)

    // 拼接IP头和TCP头
    packet := append(ipHdrBytes, tcpHdrBytes...)
    return packet, nil
}

发送SYN包与响应解析

构造好SYN包后,通过原始套接字发送到目标地址,同时需要监听响应包判断端口状态。这里我们需要再创建一个原始套接字用于接收响应:

// 扫描单个端口
func scanPort(dstIP string, dstPort uint16, timeout time.Duration) string {
    // 创建发送用的原始套接字
    sendFd, err := createRawSocket()
    if err != nil {
        return fmt.Sprintf("端口%d: 初始化失败", dstPort)
    }
    defer syscall.Close(sendFd)

    // 创建接收用的原始套接字,监听TCP协议
    recvFd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_TCP)
    if err != nil {
        return fmt.Sprintf("端口%d: 初始化接收失败", dstPort)
    }
    defer syscall.Close(recvFd)

    // 构造目标地址结构
    dstAddr := &syscall.SockaddrInet4{}
    copy(dstAddr.Addr[:], net.ParseIP(dstIP).To4())
    dstAddr.Port = int(dstPort)

    // 构造SYN包,源端口随机取10000-65535之间的端口
    srcPort := uint16(10000 + (time.Now().UnixNano() % 55535))
    packet, err := buildSYNPacket("192.168.0.1", dstIP, srcPort, dstPort)
    if err != nil {
        return fmt.Sprintf("端口%d: 构造包失败", dstPort)
    }

    // 发送SYN包
    err = syscall.Sendto(sendFd, packet, 0, dstAddr)
    if err != nil {
        return fmt.Sprintf("端口%d: 发送失败", dstPort)
    }

    // 设置接收超时
    err = syscall.SetsockoptTimeval(recvFd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{
        Sec:  int64(timeout.Seconds()),
        Usec: int64(timeout.Nanoseconds() % 1e9 / 1e3),
    })
    if err != nil {
        return fmt.Sprintf("端口%d: 设置超时失败", dstPort)
    }

    // 接收响应
    buf := make([]byte, 1024)
    for {
        n, _, err := syscall.Recvfrom(recvFd, buf, 0)
        if err != nil {
            // 超时无响应,判断为过滤
            return fmt.Sprintf("端口%d: 过滤/无响应", dstPort)
        }
        // 解析IP头部,获取源IP和协议
        if n < 20 {
            continue
        }
        srcIPRecv := binary.BigEndian.Uint32(buf[12:16])
        dstIPRecv := binary.BigEndian.Uint32(buf[16:20])
        // 匹配目标IP和源端口
        var dstIPUint uint32
        binary.Read(bytes.NewBuffer(net.ParseIP(dstIP).To4()), binary.BigEndian, &dstIPUint)
        if dstIPRecv != dstIPUint {
            continue
        }
        // 解析TCP头部
        if n < 40 {
            continue
        }
        tcpSrcPort := binary.BigEndian.Uint16(buf[20:22])
        tcpDstPort := binary.BigEndian.Uint16(buf[22:24])
        if tcpDstPort != srcPort {
            continue
        }
        // 检查TCP标志位
        tcpFlags := buf[33]
        if tcpFlags&0x12 == 0x12 { // SYN+ACK
            return fmt.Sprintf("端口%d: 开放", dstPort)
        } else if tcpFlags&0x04 == 0x04 { // RST
            return fmt.Sprintf("端口%d: 关闭", dstPort)
        }
    }
}

完整调用示例

我们可以编写一个简单的main函数,扫描指定IP的多个端口:

package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "net"
    "syscall"
    "time"
)

// 这里放入前面定义的所有结构体和方法

func main() {
    targetIP := "127.0.0.1"
    ports := []uint16{80, 22, 3306, 8080}
    timeout := 2 * time.Second

    fmt.Printf("开始扫描目标 %s 的端口n", targetIP)
    for _, port := range ports {
        result := scanPort(targetIP, port, timeout)
        fmt.Println(result)
    }
}

注意事项

  • 原始套接字需要root权限运行,普通用户执行会报错
  • 不同操作系统的syscall实现有差异,上述代码主要适用于Linux系统,Windows和macOS需要调整相关系统调用参数
  • SYN扫描属于网络探测行为,使用前需确保获得目标系统的授权,避免触犯法律法规
  • 高并发扫描时需要控制发送速率,避免对目标网络造成过大压力,也可能被防火墙拦截

通过syscall实现TCP SYN扫描可以帮助开发者深入理解TCP协议交互流程和Go语言底层系统调用的使用方式,实际应用中也可以基于这个逻辑扩展出更完善的端口扫描工具。

Go语言TCP_SYN端口扫描syscall网络编程修改时间:2026-07-17 22:10:03

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。