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

核心原理与前置知识
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