Add native protocol service plugins
测试构建 / 代码检查 (push) Has been cancelled
测试构建 / 单元测试和构建 (push) Has been cancelled
测试构建 / 构建验证 (push) Has been cancelled

This commit is contained in:
ZacharyZcR
2026-05-23 15:51:50 +08:00
parent 73cbe803c4
commit 8d30ee334c
16 changed files with 812 additions and 2 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ var (
"6000,6379,6380,6443,6666,6667," + "6000,6379,6380,6443,6666,6667," +
// Web/中间件 (7001-9999) // Web/中间件 (7001-9999)
// 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印 // 注意:9100 已移除,该端口为打印机 RAW 端口,发送数据会触发打印
"7001,7002,7474,7687,8000,8005,8008,8009,8080,8081,8086,8088,8089,8090,8161,8180,8443,8500,8834,8848,8880,8888,9000,9001,9042,9080,9090,9092,9093,9160,9200,9300,9418,9443,9999," + "7001,7002,7474,7687,8000,8005,8008,8009,8080,8081,8086,8088,8089,8090,8161,8180,8443,8500,8834,8848,8880,8883,8888,9000,9001,9042,9080,9090,9092,9093,9160,9200,9300,9418,9443,9999," +
// 管理/监控 (10000-11211) // 管理/监控 (10000-11211)
"10000,10051,10250,10255,11211," + "10000,10051,10250,10255,11211," +
// 消息队列/集群 (15672-27018) // 消息队列/集群 (15672-27018)
@@ -48,7 +48,7 @@ var (
DbPorts = "1433,1521,3306,5432,5672,5984,6379,7687,8086,9042,9093,9160,9200,11211,26379,27017,27018,61616" DbPorts = "1433,1521,3306,5432,5672,5984,6379,7687,8086,9042,9093,9160,9200,11211,26379,27017,27018,61616"
// ServicePorts 服务端口 // ServicePorts 服务端口
ServicePorts = "21,22,23,25,53,110,111,135,139,143,161,389,445,465,502,512,513,514,587,623,636,873,993,995,1433,1521,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613" ServicePorts = "21,22,23,25,53,110,111,135,139,143,161,389,445,465,502,512,513,514,587,623,636,873,993,995,1433,1521,1883,2049,2181,2222,3306,3389,5432,5672,5671,5900,5985,5986,6379,8161,8443,8883,9000,9092,9093,9200,10051,11211,15672,15671,27017,61616,61613"
// CommonPorts 常用端口 // CommonPorts 常用端口
CommonPorts = "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986" CommonPorts = "21,22,23,25,53,80,110,135,139,143,443,445,993,995,1723,3389,5060,5985,5986"
+81
View File
@@ -0,0 +1,81 @@
//go:build plugin_bacnet || !plugin_selective
package services
import (
"context"
"encoding/binary"
"fmt"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins"
)
var bacnetWhoIs = []byte{0x81, 0x0a, 0x00, 0x0c, 0x01, 0x20, 0xff, 0xff, 0x00, 0xff, 0x10, 0x08}
type BACnetPlugin struct {
plugins.BasePlugin
}
func NewBACnetPlugin() *BACnetPlugin {
return &BACnetPlugin{BasePlugin: plugins.NewBasePlugin("bacnet")}
}
func (p *BACnetPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
timeout := session.Config.Timeout
if timeout <= 0 {
timeout = 3 * time.Second
}
target := fmt.Sprintf("%s:%d", info.Host, info.Port)
conn, err := session.DialUDP(ctx, target, timeout)
if err != nil {
return &ScanResult{Success: false, Service: "bacnet"}
}
defer conn.Close()
if _, err := conn.Write(bacnetWhoIs); err != nil {
return &ScanResult{Success: false, Service: "bacnet"}
}
buf := make([]byte, 1476)
n, err := conn.Read(buf)
if err != nil {
return &ScanResult{Success: false, Service: "bacnet"}
}
banner, ok := parseBACnetResponse(buf[:n])
if !ok {
return &ScanResult{Success: false, Service: "bacnet"}
}
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "bacnet",
Banner: banner,
}
}
func parseBACnetResponse(data []byte) (string, bool) {
if len(data) < 6 || data[0] != 0x81 {
return "", false
}
length := int(binary.BigEndian.Uint16(data[2:4]))
if length != len(data) {
return "", false
}
for i := 4; i+1 < len(data); i++ {
if data[i] == 0x10 && data[i+1] == 0x00 {
return "BACnet I-Am response", true
}
}
return "", false
}
func init() {
RegisterUDPPluginWithPorts("bacnet", func() Plugin {
return NewBACnetPlugin()
}, []int{47808})
}
+16
View File
@@ -0,0 +1,16 @@
//go:build plugin_bacnet || !plugin_selective
package services
import "testing"
func TestParseBACnetResponse(t *testing.T) {
banner, ok := parseBACnetResponse([]byte{0x81, 0x0a, 0x00, 0x08, 0x01, 0x20, 0x10, 0x00})
if !ok || banner != "BACnet I-Am response" {
t.Fatalf("unexpected bacnet banner: %q ok=%v", banner, ok)
}
if _, ok := parseBACnetResponse([]byte{0x81, 0x0a, 0x00, 0x05, 0x00}); ok {
t.Fatal("unexpected match for malformed bacnet packet")
}
}
+65
View File
@@ -0,0 +1,65 @@
//go:build plugin_dns || !plugin_selective
package services
import (
"context"
"fmt"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins"
)
type DNSPlugin struct {
plugins.BasePlugin
}
func NewDNSPlugin() *DNSPlugin {
return &DNSPlugin{BasePlugin: plugins.NewBasePlugin("dns")}
}
func (p *DNSPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
timeout := session.Config.Timeout
if timeout <= 0 {
timeout = 3 * time.Second
}
target := fmt.Sprintf("%s:%d", info.Host, info.Port)
queryID := randomUint16()
query := buildDNSRootNSQuery(queryID)
conn, err := session.DialUDP(ctx, target, timeout)
if err != nil {
return &ScanResult{Success: false, Service: "dns"}
}
defer conn.Close()
if _, err := conn.Write(query); err != nil {
return &ScanResult{Success: false, Service: "dns"}
}
buf := make([]byte, 1500)
n, err := conn.Read(buf)
if err != nil || n < 12 {
return &ScanResult{Success: false, Service: "dns"}
}
banner, ok := parseDNSResponse(buf[:n], queryID)
if !ok {
return &ScanResult{Success: false, Service: "dns"}
}
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "dns",
Banner: banner,
}
}
func init() {
RegisterUDPPluginWithPorts("dns", func() Plugin {
return NewDNSPlugin()
}, []int{53})
}
+46
View File
@@ -0,0 +1,46 @@
//go:build plugin_dns || plugin_dnstcp || plugin_modbus || !plugin_selective
package services
import (
"crypto/rand"
"encoding/binary"
"fmt"
"time"
)
func randomUint16() uint16 {
var b [2]byte
if _, err := rand.Read(b[:]); err == nil {
return binary.BigEndian.Uint16(b[:])
}
return uint16(time.Now().UnixNano())
}
func buildDNSRootNSQuery(id uint16) []byte {
query := make([]byte, 17)
binary.BigEndian.PutUint16(query[0:2], id)
binary.BigEndian.PutUint16(query[2:4], 0x0100)
binary.BigEndian.PutUint16(query[4:6], 1)
query[12] = 0x00
binary.BigEndian.PutUint16(query[13:15], 2)
binary.BigEndian.PutUint16(query[15:17], 1)
return query
}
func parseDNSResponse(data []byte, id uint16) (string, bool) {
if len(data) < 12 || binary.BigEndian.Uint16(data[0:2]) != id {
return "", false
}
flags := binary.BigEndian.Uint16(data[2:4])
if flags&0x8000 == 0 {
return "", false
}
rcode := flags & 0x000f
qd := binary.BigEndian.Uint16(data[4:6])
an := binary.BigEndian.Uint16(data[6:8])
ns := binary.BigEndian.Uint16(data[8:10])
ar := binary.BigEndian.Uint16(data[10:12])
return fmt.Sprintf("DNS response rcode=%d qd=%d an=%d ns=%d ar=%d", rcode, qd, an, ns, ar), true
}
+33
View File
@@ -0,0 +1,33 @@
//go:build plugin_dns || !plugin_selective
package services
import (
"encoding/binary"
"strings"
"testing"
)
func TestDNSQueryAndResponse(t *testing.T) {
const id uint16 = 0x1234
query := buildDNSRootNSQuery(id)
if len(query) != 17 {
t.Fatalf("unexpected dns query length: %d", len(query))
}
if binary.BigEndian.Uint16(query[0:2]) != id || binary.BigEndian.Uint16(query[13:15]) != 2 {
t.Fatalf("unexpected dns query: %#v", query)
}
resp := make([]byte, 12)
binary.BigEndian.PutUint16(resp[0:2], id)
binary.BigEndian.PutUint16(resp[2:4], 0x8180)
binary.BigEndian.PutUint16(resp[4:6], 1)
binary.BigEndian.PutUint16(resp[6:8], 2)
binary.BigEndian.PutUint16(resp[8:10], 3)
binary.BigEndian.PutUint16(resp[10:12], 4)
banner, ok := parseDNSResponse(resp, id)
if !ok || !strings.Contains(banner, "rcode=0") || !strings.Contains(banner, "an=2") {
t.Fatalf("unexpected dns banner: %q ok=%v", banner, ok)
}
}
+79
View File
@@ -0,0 +1,79 @@
//go:build plugin_dnstcp || !plugin_selective
package services
import (
"context"
"encoding/binary"
"fmt"
"io"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins"
)
type DNSTCPPlugin struct {
plugins.BasePlugin
}
func NewDNSTCPPlugin() *DNSTCPPlugin {
return &DNSTCPPlugin{BasePlugin: plugins.NewBasePlugin("dnstcp")}
}
func (p *DNSTCPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
timeout := session.Config.Timeout
if timeout <= 0 {
timeout = 3 * time.Second
}
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
if err != nil {
return &ScanResult{Success: false, Service: "dns"}
}
defer conn.Close()
queryID := randomUint16()
query := buildDNSRootNSQuery(queryID)
frame := make([]byte, 2, len(query)+2)
binary.BigEndian.PutUint16(frame, uint16(len(query)))
frame = append(frame, query...)
_ = conn.SetDeadline(time.Now().Add(timeout))
if _, err := conn.Write(frame); err != nil {
return &ScanResult{Success: false, Service: "dns"}
}
var lenBuf [2]byte
if _, err := io.ReadFull(conn, lenBuf[:]); err != nil {
return &ScanResult{Success: false, Service: "dns"}
}
respLen := int(binary.BigEndian.Uint16(lenBuf[:]))
if respLen < 12 || respLen > 4096 {
return &ScanResult{Success: false, Service: "dns"}
}
resp := make([]byte, respLen)
if _, err := io.ReadFull(conn, resp); err != nil {
return &ScanResult{Success: false, Service: "dns"}
}
banner, ok := parseDNSResponse(resp, queryID)
if !ok {
return &ScanResult{Success: false, Service: "dns"}
}
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "dns",
Banner: banner,
}
}
func init() {
RegisterPluginWithPorts("dnstcp", func() Plugin {
return NewDNSTCPPlugin()
}, []int{53})
}
+19
View File
@@ -0,0 +1,19 @@
//go:build plugin_dnstcp || !plugin_selective
package services
import (
"encoding/binary"
"testing"
)
func TestDNSTCPFrame(t *testing.T) {
query := buildDNSRootNSQuery(0x4321)
frame := make([]byte, 2, len(query)+2)
binary.BigEndian.PutUint16(frame, uint16(len(query)))
frame = append(frame, query...)
if binary.BigEndian.Uint16(frame[:2]) != uint16(len(query)) {
t.Fatalf("unexpected dns tcp length prefix: %#v", frame[:2])
}
}
+104
View File
@@ -0,0 +1,104 @@
//go:build plugin_modbus || !plugin_selective
package services
import (
"context"
"encoding/binary"
"fmt"
"io"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins"
)
type ModbusPlugin struct {
plugins.BasePlugin
}
func NewModbusPlugin() *ModbusPlugin {
return &ModbusPlugin{BasePlugin: plugins.NewBasePlugin("modbus")}
}
func (p *ModbusPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
timeout := session.Config.Timeout
if timeout <= 0 {
timeout = 3 * time.Second
}
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
if err != nil {
return &ScanResult{Success: false, Service: "modbus"}
}
defer conn.Close()
txID := randomUint16()
req := buildModbusDeviceIDRequest(txID)
_ = conn.SetDeadline(time.Now().Add(timeout))
if _, err := conn.Write(req); err != nil {
return &ScanResult{Success: false, Service: "modbus"}
}
header := make([]byte, 7)
if _, err := io.ReadFull(conn, header); err != nil {
return &ScanResult{Success: false, Service: "modbus"}
}
length := int(binary.BigEndian.Uint16(header[4:6]))
if length < 2 || length > 260 {
return &ScanResult{Success: false, Service: "modbus"}
}
body := make([]byte, length-1)
if _, err := io.ReadFull(conn, body); err != nil {
return &ScanResult{Success: false, Service: "modbus"}
}
banner, ok := parseModbusResponse(header, body, txID)
if !ok {
return &ScanResult{Success: false, Service: "modbus"}
}
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "modbus",
Banner: banner,
}
}
func buildModbusDeviceIDRequest(txID uint16) []byte {
req := make([]byte, 11)
binary.BigEndian.PutUint16(req[0:2], txID)
binary.BigEndian.PutUint16(req[2:4], 0)
binary.BigEndian.PutUint16(req[4:6], 5)
req[6] = 0xff
req[7] = 0x2b
req[8] = 0x0e
req[9] = 0x01
req[10] = 0x00
return req
}
func parseModbusResponse(header, body []byte, txID uint16) (string, bool) {
if len(header) < 7 || len(body) < 1 {
return "", false
}
if binary.BigEndian.Uint16(header[0:2]) != txID || binary.BigEndian.Uint16(header[2:4]) != 0 {
return "", false
}
switch body[0] {
case 0x2b:
return "Modbus TCP device identification response", true
case 0xab:
return "Modbus TCP exception response", true
default:
return "", false
}
}
func init() {
RegisterPluginWithPorts("modbus", func() Plugin {
return NewModbusPlugin()
}, []int{502})
}
+22
View File
@@ -0,0 +1,22 @@
//go:build plugin_modbus || !plugin_selective
package services
import (
"encoding/binary"
"strings"
"testing"
)
func TestModbusDeviceIDRequestAndResponse(t *testing.T) {
req := buildModbusDeviceIDRequest(0x1001)
if len(req) != 11 || binary.BigEndian.Uint16(req[0:2]) != 0x1001 || req[7] != 0x2b {
t.Fatalf("unexpected modbus request: %#v", req)
}
header := []byte{0x10, 0x01, 0x00, 0x00, 0x00, 0x03, 0xff}
banner, ok := parseModbusResponse(header, []byte{0x2b, 0x0e}, 0x1001)
if !ok || !strings.Contains(banner, "Modbus TCP") {
t.Fatalf("unexpected modbus banner: %q ok=%v", banner, ok)
}
}
+114
View File
@@ -0,0 +1,114 @@
//go:build plugin_mqtt || !plugin_selective
package services
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins"
)
var mqttConnectPacket = []byte{
0x10, 0x0c,
0x00, 0x04, 'M', 'Q', 'T', 'T',
0x04,
0x02,
0x00, 0x00,
0x00, 0x00,
}
type MQTTPlugin struct {
plugins.BasePlugin
}
func NewMQTTPlugin() *MQTTPlugin {
return &MQTTPlugin{BasePlugin: plugins.NewBasePlugin("mqtt")}
}
func (p *MQTTPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
timeout := session.Config.Timeout
if timeout <= 0 {
timeout = 3 * time.Second
}
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
if err != nil {
return &ScanResult{Success: false, Service: "mqtt"}
}
defer conn.Close()
if info.Port == 8883 {
_ = conn.SetDeadline(time.Now().Add(timeout))
conn, err = p.wrapTLS(ctx, conn)
if err != nil {
return &ScanResult{Success: false, Service: "mqtt"}
}
defer conn.Close()
}
_ = conn.SetDeadline(time.Now().Add(timeout))
if _, err := conn.Write(mqttConnectPacket); err != nil {
return &ScanResult{Success: false, Service: "mqtt"}
}
header := make([]byte, 4)
if _, err := io.ReadFull(conn, header); err != nil {
return &ScanResult{Success: false, Service: "mqtt"}
}
banner, ok := parseMQTTConnack(header)
if !ok {
return &ScanResult{Success: false, Service: "mqtt"}
}
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "mqtt",
Banner: banner,
}
}
func (p *MQTTPlugin) wrapTLS(ctx context.Context, conn net.Conn) (net.Conn, error) {
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
if err := tlsConn.HandshakeContext(ctx); err != nil {
return nil, err
}
return tlsConn, nil
}
func parseMQTTConnack(data []byte) (string, bool) {
if len(data) < 4 || data[0] != 0x20 || data[1] != 0x02 {
return "", false
}
switch data[3] {
case 0x00:
return "MQTT CONNACK accepted", true
case 0x01:
return "MQTT CONNACK unacceptable protocol version", true
case 0x02:
return "MQTT CONNACK identifier rejected", true
case 0x03:
return "MQTT CONNACK server unavailable", true
case 0x04:
return "MQTT CONNACK bad username or password", true
case 0x05:
return "MQTT CONNACK not authorized", true
default:
return fmt.Sprintf("MQTT CONNACK return_code=%d", data[3]), true
}
}
func init() {
RegisterPluginWithPorts("mqtt", func() Plugin {
return NewMQTTPlugin()
}, []int{1883, 8883})
}
+19
View File
@@ -0,0 +1,19 @@
//go:build plugin_mqtt || !plugin_selective
package services
import (
"strings"
"testing"
)
func TestParseMQTTConnack(t *testing.T) {
banner, ok := parseMQTTConnack([]byte{0x20, 0x02, 0x00, 0x05})
if !ok || !strings.Contains(banner, "not authorized") {
t.Fatalf("unexpected mqtt banner: %q ok=%v", banner, ok)
}
if _, ok := parseMQTTConnack([]byte{0x10, 0x02, 0x00, 0x00}); ok {
t.Fatal("unexpected match for non-connack packet")
}
}
+95
View File
@@ -0,0 +1,95 @@
//go:build plugin_tftp || !plugin_selective
package services
import (
"context"
"fmt"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins"
)
type TFTPPlugin struct {
plugins.BasePlugin
}
func NewTFTPPlugin() *TFTPPlugin {
return &TFTPPlugin{BasePlugin: plugins.NewBasePlugin("tftp")}
}
func (p *TFTPPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
timeout := session.Config.Timeout
if timeout <= 0 {
timeout = 3 * time.Second
}
target := fmt.Sprintf("%s:%d", info.Host, info.Port)
conn, err := session.DialUDP(ctx, target, timeout)
if err != nil {
return &ScanResult{Success: false, Service: "tftp"}
}
defer conn.Close()
if _, err := conn.Write(buildTFTPReadRequest("probe")); err != nil {
return &ScanResult{Success: false, Service: "tftp"}
}
buf := make([]byte, 516)
n, err := conn.Read(buf)
if err != nil || n < 4 {
return &ScanResult{Success: false, Service: "tftp"}
}
banner, ok := parseTFTPResponse(buf[:n])
if !ok {
return &ScanResult{Success: false, Service: "tftp"}
}
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "tftp",
Banner: banner,
}
}
func buildTFTPReadRequest(filename string) []byte {
req := []byte{0x00, 0x01}
req = append(req, filename...)
req = append(req, 0x00)
req = append(req, "octet"...)
req = append(req, 0x00)
return req
}
func parseTFTPResponse(data []byte) (string, bool) {
if len(data) < 4 || data[0] != 0x00 {
return "", false
}
opcode := data[1]
switch opcode {
case 0x03:
return "TFTP DATA response", true
case 0x05:
msg := strings.TrimRight(string(data[4:]), "\x00")
if len(msg) > 160 {
msg = msg[:160]
}
if msg == "" {
msg = "error response"
}
return fmt.Sprintf("TFTP %s", msg), true
default:
return "", false
}
}
func init() {
RegisterUDPPluginWithPorts("tftp", func() Plugin {
return NewTFTPPlugin()
}, []int{69})
}
+21
View File
@@ -0,0 +1,21 @@
//go:build plugin_tftp || !plugin_selective
package services
import (
"strings"
"testing"
)
func TestTFTPReadRequestAndResponse(t *testing.T) {
req := buildTFTPReadRequest("probe")
want := []byte{0x00, 0x01, 'p', 'r', 'o', 'b', 'e', 0x00, 'o', 'c', 't', 'e', 't', 0x00}
if string(req) != string(want) {
t.Fatalf("unexpected tftp request: %#v", req)
}
banner, ok := parseTFTPResponse([]byte{0x00, 0x05, 0x00, 0x01, 'n', 'o', 't', ' ', 'f', 'o', 'u', 'n', 'd', 0x00})
if !ok || !strings.Contains(banner, "not found") {
t.Fatalf("unexpected tftp banner: %q ok=%v", banner, ok)
}
}
+80
View File
@@ -0,0 +1,80 @@
//go:build plugin_zookeeper || !plugin_selective
package services
import (
"context"
"fmt"
"strings"
"time"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/plugins"
)
type ZooKeeperPlugin struct {
plugins.BasePlugin
}
func NewZooKeeperPlugin() *ZooKeeperPlugin {
return &ZooKeeperPlugin{BasePlugin: plugins.NewBasePlugin("zookeeper")}
}
func (p *ZooKeeperPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *ScanResult {
timeout := session.Config.Timeout
if timeout <= 0 {
timeout = 3 * time.Second
}
addr := fmt.Sprintf("%s:%d", info.Host, info.Port)
conn, err := session.DialTCP(ctx, "tcp", addr, timeout)
if err != nil {
return &ScanResult{Success: false, Service: "zookeeper"}
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(timeout))
if _, err := conn.Write([]byte("ruok")); err != nil {
return &ScanResult{Success: false, Service: "zookeeper"}
}
buf := make([]byte, 512)
n, err := conn.Read(buf)
if err != nil || n == 0 {
return &ScanResult{Success: false, Service: "zookeeper"}
}
banner, ok := parseZooKeeperResponse(buf[:n])
if !ok {
return &ScanResult{Success: false, Service: "zookeeper"}
}
return &ScanResult{
Success: true,
Type: plugins.ResultTypeService,
Service: "zookeeper",
Banner: banner,
}
}
func parseZooKeeperResponse(data []byte) (string, bool) {
resp := strings.TrimSpace(string(data))
if resp == "imok" {
return "ZooKeeper ruok=imok", true
}
lower := strings.ToLower(resp)
if strings.Contains(lower, "zookeeper") || strings.Contains(lower, "zk_version") ||
strings.Contains(lower, "mode:") || strings.Contains(lower, "not in the whitelist") {
if len(resp) > 200 {
resp = resp[:200]
}
return resp, true
}
return "", false
}
func init() {
RegisterPluginWithPorts("zookeeper", func() Plugin {
return NewZooKeeperPlugin()
}, []int{2181})
}
+16
View File
@@ -0,0 +1,16 @@
//go:build plugin_zookeeper || !plugin_selective
package services
import "testing"
func TestParseZooKeeperResponse(t *testing.T) {
banner, ok := parseZooKeeperResponse([]byte("imok"))
if !ok || banner != "ZooKeeper ruok=imok" {
t.Fatalf("unexpected zookeeper banner: %q ok=%v", banner, ok)
}
if _, ok := parseZooKeeperResponse([]byte("hello")); ok {
t.Fatal("unexpected match for non-zookeeper response")
}
}