实现Ping和TCP端口扫描功能
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+113
-21
@@ -4,11 +4,15 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"ip_scan_go/internal/scan"
|
||||
)
|
||||
|
||||
const appVersion = "V0.0.1"
|
||||
@@ -31,6 +35,7 @@ type scanForm struct {
|
||||
scanBtn *widget.Button
|
||||
pingBtn *widget.Button
|
||||
keypadBtn []*widget.Button
|
||||
dispatcher *scan.Dispatcher
|
||||
scanning bool
|
||||
}
|
||||
|
||||
@@ -108,36 +113,108 @@ func (f *scanForm) buildControlRow() fyne.CanvasObject {
|
||||
|
||||
func (f *scanForm) toggleScan() {
|
||||
if !f.scanning {
|
||||
f.scanning = true
|
||||
f.scanBtn.SetText("停止扫描")
|
||||
f.pingBtn.Disable()
|
||||
f.setInputsEnabled(false)
|
||||
f.progress.SetValue(0)
|
||||
f.output.Append("开始扫描... (TODO: 接入扫描逻辑)\n")
|
||||
ips, err := scan.ParseIPs(f.ipList.Text)
|
||||
if err != nil {
|
||||
f.appendOutput("IP解析错误: " + err.Error() + "\n")
|
||||
return
|
||||
}
|
||||
ports, err := scan.ParsePorts(f.portList.Text)
|
||||
if err != nil {
|
||||
f.appendOutput("端口解析错误: " + err.Error() + "\n")
|
||||
return
|
||||
}
|
||||
if len(ips) == 0 || len(ports) == 0 {
|
||||
f.appendOutput("请输入IP和端口\n")
|
||||
return
|
||||
}
|
||||
f.startScan(false, ips, ports)
|
||||
} else {
|
||||
f.scanning = false
|
||||
f.scanBtn.SetText("开始扫描")
|
||||
f.pingBtn.Enable()
|
||||
f.setInputsEnabled(true)
|
||||
f.output.Append("已停止扫描\n")
|
||||
f.stopScan()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *scanForm) togglePing() {
|
||||
if !f.scanning {
|
||||
f.scanning = true
|
||||
ips, err := scan.ParseIPs(f.ipList.Text)
|
||||
if err != nil {
|
||||
f.appendOutput("IP解析错误: " + err.Error() + "\n")
|
||||
return
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
f.appendOutput("请输入IP\n")
|
||||
return
|
||||
}
|
||||
f.startScan(true, ips, nil)
|
||||
} else {
|
||||
f.stopScan()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *scanForm) startScan(pingOnly bool, ips []string, ports []int) {
|
||||
f.scanning = true
|
||||
if pingOnly {
|
||||
f.pingBtn.SetText("停止Ping")
|
||||
f.scanBtn.Disable()
|
||||
f.setInputsEnabled(false)
|
||||
f.progress.SetValue(0)
|
||||
f.output.Append("开始Ping... (TODO: 接入扫描逻辑)\n")
|
||||
} else {
|
||||
f.scanning = false
|
||||
f.pingBtn.SetText("仅Ping")
|
||||
f.scanBtn.Enable()
|
||||
f.setInputsEnabled(true)
|
||||
f.output.Append("已停止Ping\n")
|
||||
f.scanBtn.SetText("停止扫描")
|
||||
f.pingBtn.Disable()
|
||||
}
|
||||
f.setInputsEnabled(false)
|
||||
f.progress.SetValue(0)
|
||||
|
||||
timeout := time.Duration(parseIntEntry(f.timeout, 100)) * time.Millisecond
|
||||
workers := parseIntEntry(f.threads, 64)
|
||||
mode := "扫描"
|
||||
if pingOnly {
|
||||
mode = "Ping"
|
||||
}
|
||||
f.appendOutput(fmt.Sprintf("开始%s (IP:%d 线程:%d 超时:%s)\n", mode, len(ips), workers, timeout))
|
||||
|
||||
d := &scan.Dispatcher{
|
||||
IPs: ips,
|
||||
Ports: ports,
|
||||
Timeout: timeout,
|
||||
Workers: workers,
|
||||
PingOnly: pingOnly,
|
||||
OnResult: func(r scan.Result) {
|
||||
fyne.Do(func() {
|
||||
f.appendOutput(r.Text + "\n")
|
||||
})
|
||||
},
|
||||
OnProgress: func(p scan.Progress) {
|
||||
pct := float64(p.Done) / float64(p.Total)
|
||||
fyne.Do(func() {
|
||||
f.progress.SetValue(pct)
|
||||
})
|
||||
},
|
||||
OnFinish: func() {
|
||||
fyne.Do(func() {
|
||||
f.resetAfterScan(pingOnly)
|
||||
f.appendOutput(fmt.Sprintf("%s完成\n", mode))
|
||||
})
|
||||
},
|
||||
}
|
||||
f.dispatcher = d
|
||||
go d.Start()
|
||||
}
|
||||
|
||||
func (f *scanForm) stopScan() {
|
||||
if f.dispatcher != nil {
|
||||
f.dispatcher.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *scanForm) resetAfterScan(pingOnly bool) {
|
||||
f.scanning = false
|
||||
if pingOnly {
|
||||
f.pingBtn.SetText("仅Ping")
|
||||
} else {
|
||||
f.scanBtn.SetText("开始扫描")
|
||||
}
|
||||
f.scanBtn.Enable()
|
||||
f.pingBtn.Enable()
|
||||
f.setInputsEnabled(true)
|
||||
f.dispatcher = nil
|
||||
}
|
||||
|
||||
func (f *scanForm) setInputsEnabled(enabled bool) {
|
||||
@@ -162,7 +239,14 @@ func (f *scanForm) setInputsEnabled(enabled bool) {
|
||||
}
|
||||
|
||||
func (f *scanForm) saveOutput() {
|
||||
f.output.Append("(TODO: 保存输出到文件)\n")
|
||||
f.appendOutput("(TODO: 保存输出到文件)\n")
|
||||
}
|
||||
|
||||
func (f *scanForm) appendOutput(text string) {
|
||||
f.output.Append(text)
|
||||
f.output.CursorRow = strings.Count(f.output.Text, "\n")
|
||||
f.output.CursorColumn = 0
|
||||
f.output.Refresh()
|
||||
}
|
||||
|
||||
func (f *scanForm) openURL(urlStr string) {
|
||||
@@ -185,3 +269,11 @@ func newIntEntry(val, min, max int) *widget.Entry {
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func parseIntEntry(e *widget.Entry, fallback int) int {
|
||||
n, err := strconv.Atoi(e.Text)
|
||||
if err != nil || n <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user