实现Ping和TCP端口扫描功能

This commit is contained in:
2026-07-17 19:00:47 +08:00
parent 4a2086c6f1
commit c858e8e311
6 changed files with 516 additions and 23 deletions
+141
View File
@@ -0,0 +1,141 @@
package scan
import (
"strconv"
"sync"
"sync/atomic"
"time"
)
type Result struct {
Text string
}
type Progress struct {
Done int
Total int
}
type Dispatcher struct {
IPs []string
Ports []int
Timeout time.Duration
Workers int
PingOnly bool
OnResult func(Result)
OnProgress func(Progress)
OnFinish func()
stop chan struct{}
done sync.WaitGroup
counter int64
}
func (d *Dispatcher) Start() {
d.stop = make(chan struct{})
tasks := d.buildTasks()
total := len(tasks)
if total == 0 {
if d.OnFinish != nil {
d.OnFinish()
}
return
}
workers := d.Workers
if workers <= 0 {
workers = 1
}
taskCh := make(chan task)
for i := 0; i < workers; i++ {
d.done.Add(1)
go d.worker(taskCh, total)
}
feed:
for _, t := range tasks {
select {
case <-d.stop:
break feed
case taskCh <- t:
}
}
close(taskCh)
d.done.Wait()
if d.OnFinish != nil {
d.OnFinish()
}
}
func (d *Dispatcher) worker(taskCh <-chan task, total int) {
defer d.done.Done()
var pinger *Pinger
if d.PingOnly {
pinger, _ = NewPinger()
defer pinger.Close()
}
for task := range taskCh {
select {
case <-d.stop:
return
default:
}
ok := false
if task.ping {
if pinger != nil {
ok = pinger.Ping(task.ip, d.Timeout)
}
} else {
ok = TCPProbe(task.ip, task.port, d.Timeout)
}
if ok && d.OnResult != nil {
d.OnResult(Result{Text: task.label})
}
done := int(atomic.AddInt64(&d.counter, 1))
if d.OnProgress != nil {
d.OnProgress(Progress{Done: done, Total: total})
}
}
}
func (d *Dispatcher) Stop() {
select {
case <-d.stop:
default:
close(d.stop)
}
}
type task struct {
ip string
port int
label string
ping bool
}
func (d *Dispatcher) buildTasks() []task {
var tasks []task
if d.PingOnly {
for _, ip := range d.IPs {
tasks = append(tasks, task{ip: ip, label: ip, ping: true})
}
} else {
for _, ip := range d.IPs {
for _, port := range d.Ports {
tasks = append(tasks, task{
ip: ip,
port: port,
label: ip + ":" + strconv.Itoa(port),
})
}
}
}
return tasks
}
+108
View File
@@ -0,0 +1,108 @@
package scan
import (
"fmt"
"net"
"sync/atomic"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
type Pinger struct {
conn *icmp.PacketConn
seq atomic.Uint32
}
func NewPinger() (*Pinger, error) {
conn, err := icmp.ListenPacket("udp4", "0.0.0.0")
if err != nil {
return nil, fmt.Errorf("icmp listen: %w", err)
}
return &Pinger{conn: conn}, nil
}
func (p *Pinger) Close() {
if p.conn != nil {
p.conn.Close()
}
}
func (p *Pinger) Ping(ip string, timeout time.Duration) bool {
dst, err := net.ResolveIPAddr("ip4", ip)
if err != nil {
return false
}
var target net.Addr
switch p.conn.LocalAddr().(type) {
case *net.UDPAddr:
target = &net.UDPAddr{IP: dst.IP}
case *net.IPAddr:
target = &net.IPAddr{IP: dst.IP}
default:
target = dst
}
seq := uint16(p.seq.Add(1))
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: 0,
Seq: int(seq),
Data: []byte("PING"),
},
}
buf, err := msg.Marshal(nil)
if err != nil {
return false
}
if _, err := p.conn.WriteTo(buf, target); err != nil {
return false
}
deadline := time.Now().Add(timeout)
rb := make([]byte, 1500)
for {
remaining := time.Until(deadline)
if remaining <= 0 {
return false
}
_ = p.conn.SetReadDeadline(time.Now().Add(remaining))
n, peer, err := p.conn.ReadFrom(rb)
if err != nil {
return false
}
rm, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply.Protocol(), rb[:n])
if err != nil {
continue
}
if rm.Type != ipv4.ICMPTypeEchoReply {
continue
}
var peerIP net.IP
switch a := peer.(type) {
case *net.UDPAddr:
peerIP = a.IP
case *net.IPAddr:
peerIP = a.IP
default:
peerIP = net.ParseIP(peer.String())
}
if peerIP != nil && peerIP.Equal(dst.IP) {
return true
}
}
}
func Ping(ip string, timeout time.Duration) (bool, error) {
p, err := NewPinger()
if err != nil {
return false, err
}
defer p.Close()
return p.Ping(ip, timeout), nil
}
+133
View File
@@ -0,0 +1,133 @@
package scan
import (
"bufio"
"fmt"
"net"
"net/netip"
"regexp"
"strconv"
"strings"
)
var (
reIPv4 = regexp.MustCompile(`^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$`)
reIPv4Range = regexp.MustCompile(`^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)-((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$`)
reCIDR = regexp.MustCompile(`^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)/(0?[0-9]|1[0-9]|2[0-9]|3[0-2])$`)
reIPv6 = regexp.MustCompile(`^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$`)
reDomain = regexp.MustCompile(`^([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,}$`)
rePort = regexp.MustCompile(`^(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$`)
)
func ParseIPs(text string) ([]string, error) {
var ips []string
sc := bufio.NewScanner(strings.NewReader(text))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
continue
}
switch {
case reIPv4.MatchString(line):
ips = append(ips, line)
case reIPv4Range.MatchString(line):
parts := strings.SplitN(line, "-", 2)
a, err := netip.ParseAddr(parts[0])
if err != nil {
return nil, fmt.Errorf("无效IP段: %s", line)
}
b, err := netip.ParseAddr(parts[1])
if err != nil {
return nil, fmt.Errorf("无效IP段: %s", line)
}
ai := a.As4()
bi := b.As4()
aInt := uint32(ai[0])<<24 | uint32(ai[1])<<16 | uint32(ai[2])<<8 | uint32(ai[3])
bInt := uint32(bi[0])<<24 | uint32(bi[1])<<16 | uint32(bi[2])<<8 | uint32(bi[3])
if aInt > bInt {
aInt, bInt = bInt, aInt
}
for i := aInt; i <= bInt; i++ {
ip := netip.AddrFrom4([4]byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i)})
ips = append(ips, ip.String())
}
case reCIDR.MatchString(line):
baseIP, ipNet, err := net.ParseCIDR(line)
if err != nil {
return nil, fmt.Errorf("无效CIDR: %s", line)
}
if baseIP.To4() == nil {
return nil, fmt.Errorf("仅支持IPv4 CIDR: %s", line)
}
ips = append(ips, cidrIPs(ipNet)...)
case reIPv6.MatchString(line):
ips = append(ips, line)
case reDomain.MatchString(line):
ips = append(ips, line)
default:
return nil, fmt.Errorf("无法识别的目标: %s", line)
}
}
return ips, sc.Err()
}
func cidrIPs(ipNet *net.IPNet) []string {
var ips []string
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); inc(ip) {
ips = append(ips, ip.String())
}
if len(ips) > 0 {
ips = ips[1:]
}
if len(ips) > 0 {
ips = ips[:len(ips)-1]
}
return ips
}
func inc(ip net.IP) {
for j := len(ip) - 1; j >= 0; j-- {
ip[j]++
if ip[j] > 0 {
break
}
}
}
func ParsePorts(text string) ([]int, error) {
var ports []int
sc := bufio.NewScanner(strings.NewReader(text))
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
continue
}
for _, seg := range strings.Split(line, ";") {
seg = strings.TrimSpace(seg)
if seg == "" {
continue
}
if strings.Contains(seg, "-") {
parts := strings.SplitN(seg, "-", 2)
a, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
b, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
if err1 != nil || err2 != nil || a < 0 || a > 65535 || b < 0 || b > 65535 {
return nil, fmt.Errorf("无效端口范围: %s", seg)
}
if a > b {
a, b = b, a
}
for p := a; p <= b; p++ {
ports = append(ports, p)
}
} else {
if !rePort.MatchString(seg) {
return nil, fmt.Errorf("无效端口: %s", seg)
}
p, _ := strconv.Atoi(seg)
ports = append(ports, p)
}
}
}
return ports, sc.Err()
}
+17
View File
@@ -0,0 +1,17 @@
package scan
import (
"net"
"strconv"
"time"
)
func TCPProbe(ip string, port int, timeout time.Duration) bool {
addr := net.JoinHostPort(ip, strconv.Itoa(port))
conn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
return false
}
conn.Close()
return true
}