Skip to content

Commit 772da28

Browse files
committed
Merge pull request #11 from eagle00789/master
Bugfix for bug 9 and finish of new nmap sensor
2 parents 1463691 + eded385 commit 772da28

File tree

4 files changed

+243
-81
lines changed

4 files changed

+243
-81
lines changed

README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ Installation
2323
- copy the miniprobe folder to your linux machine
2424
- run the probe installer (e.g. "python probe_installer.py")
2525

26-
The miniprobe should now be started. You should also be able to start/stop the same using the command /etc/init.d/probe.sh start resp. /etc/init.d/probe.sh stop
26+
The miniprobe should now be started. You should also be able to start/stop the same using the command
27+
28+
sudo service prtgprobe start
29+
30+
or
31+
32+
sudo service prtgprobe stop
33+
2734

2835
Instalation of DS18B20
2936
----------------------
@@ -39,6 +46,23 @@ Setup:
3946
- Run the installscript of the probe and answer Yes to the question if you want to use the Raspberry Pi temperature sensor.
4047
- The installscript will now make a change to the raspberry boot process to include a special library and it will reboot the Raspberry. After the reboot, run the installer again and answer the same question again. It will now (if all is correct) detect your DS18B20 (using it's own unique serial number) and just confirm that this is correct by presing <Return> on your keyboard.
4148

49+
Current available sensors
50+
-------------------------
51+
- CPU Load (for probe only)
52+
- CPU Temperature (for probe only)
53+
- Disk Space (for probe only)
54+
- DNS for the following records: A, AAAA, CNAME, SRV, SOA, NS, MX, PTR
55+
- External IP to get the external and internal ip for the probe
56+
- HTTP
57+
- Linux Updates to check for the number of available updates (for probe only)
58+
- Memory (for probe only)
59+
- NMAP to get available systems to monitor (currently using ping only)
60+
- Ping to check if a host/system is up
61+
- Port to check if a specific port is available
62+
- Probe Health for overal probe health, combines several other sensors into 1
63+
- SNMP Custom to monitor a system using SNMP OID
64+
- SNMP Traffic to monitor traffic on the probe
65+
4266
Debugging
4367
---------
4468

probe.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def main():
186186
request_data.close()
187187
json_payload_data = []
188188
except requests.exceptions.Timeout:
189-
logging.error("DATA Timeout: " + json_payload_data)
189+
logging.error("DATA Timeout: " + str(json_payload_data).strip('[]'))
190190
except Exception as announce_error:
191191
logging.error(announce_error)
192192
if len(json_response) > 10:

probe_installer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def init_script(script_path, user):
105105
return init_script_tpl.read() % (script_path, user)
106106

107107
def write_load_list(ds18b20_sensors, other_sensors):
108-
default_sensors = "Ping,HTTP,Port,SNMPCustom,CPULoad,Memory,Diskspace,SNMPTraffic,CPUTemp,Probehealth,External_IP,aDNS,APT"
108+
default_sensors = "Ping,HTTP,Port,SNMPCustom,CPULoad,Memory,Diskspace,SNMPTraffic,CPUTemp,Probehealth,External_IP,aDNS,APT,NMAP"
109109
if not (other_sensors == ""):
110110
default_sensors = default_sensors + "," + other_sensors
111111
f=open("./sensors/__init__.py","w")

sensors/nmap.py

Lines changed: 216 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,16 @@
2323
import socket
2424
import logging
2525
import gc
26+
import re
27+
import os
28+
import sys
29+
import struct
30+
import select
31+
from itertools import islice
2632

2733
class NMAP(object):
34+
ICMP_ECHO_REQUEST = 8
35+
2836
def __init__(self):
2937
pass
3038

@@ -43,32 +51,24 @@ def get_sensordef():
4351
sensordefinition = {
4452
"kind": NMAP.get_kind(),
4553
"name": "NMAP",
46-
"description": "Checks the availability of a port (range) on one or more systems.",
47-
"help": "Checks the availability of a port (range) on one or more systems and logs this to a separate logfile on the miniprobe.",
54+
"description": "Checks the availability of systems.",
55+
"help": "Checks the availability of systems on a network and logs this to a separate logfile on the miniprobe.",
4856
"tag": "mpnmapsensor",
4957
"groups": [
5058
{
51-
"name": "portspecific",
52-
"caption": "Port specific",
59+
"name": "nmapspecific",
60+
"caption": "NMAP specific",
5361
"fields": [
5462
{
5563
"type": "integer",
5664
"name": "timeout",
57-
"caption": "Timeout (in s)",
65+
"caption": "Timeout (in ms)",
5866
"required": "1",
59-
"default": 60,
60-
"minimum": 1,
61-
"maximum": 900,
67+
"default": 50,
68+
"minimum": 10,
69+
"maximum": 1000,
6270
"help": "If the reply takes longer than this value the request is aborted "
63-
"and an error message is triggered. Max. value is 900 sec. (=15 min.)"
64-
},
65-
{
66-
"type": "edit",
67-
"name": "port",
68-
"caption": "Port or port range",
69-
"required": "1",
70-
"default": "1-1024",
71-
"help": "Specify the port or the port range devided by a - (for example: 1-1024)"
71+
"and an error message is triggered. Max. value is 1000 ms. (=1 sec.)"
7272
},
7373
{
7474
"type": "edit",
@@ -79,83 +79,221 @@ def get_sensordef():
7979
"help": "Specify the ip-address or a range of addresses using one of the following notations:[br]Single: 192.168.1.1[br]CIDR: 192.168.1.0/24[br]- separated: 192.168.1.1-192.168.1.100"
8080
}
8181
]
82-
},
83-
{
84-
"name": "mail",
85-
"caption": "Mail Settings",
86-
"fields": [
87-
{
88-
"type": "edit",
89-
"name": "email",
90-
"caption": "E-Mailaddress",
91-
"required": "1",
92-
"help": "Specify the e-mailaddress to which the NMAP report should be sent to when the scanning has been finished"
93-
}
94-
]
9582
}
9683
]
9784
}
9885
return sensordefinition
9986

100-
def portrange(self, target, timeout, start, end):
101-
remote_server = socket.gethostbyname(target)
102-
numberofports = int(end) - int(start)
103-
result = 1234
104-
a = 0
105-
start_time = time.time()
106-
for port in range(int(start), int(end)):
107-
try:
108-
conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
109-
conn.settimeout(float(timeout))
110-
result = conn.connect_ex((remote_server, int(port)))
111-
conn.close()
112-
except socket.gaierror as e:
113-
print e
114-
except socket.timeout as e:
115-
print e
116-
except Exception as e:
117-
print "test %s" % e
118-
if result == 0:
119-
a += 1
120-
else:
121-
raise Exception('port %s not open' % port)
122-
123-
end_time = time.time()
124-
response_time = (end_time - start_time) * 1000
125-
if a == numberofports:
126-
channel_list = [
127-
{
128-
"name": "Available",
129-
"mode": "float",
130-
"kind": "TimeResponse",
131-
"value": float(response_time)
132-
}
133-
]
134-
return channel_list
135-
else:
136-
raise Exception
137-
13887
@staticmethod
139-
def get_data(data):
140-
port = NMAP()
88+
def get_data(data, out_queue):
89+
nmap = NMAP()
90+
error = False
91+
tmpMessage = ""
92+
cidr = ""
93+
alive_str = ""
94+
alive_cnt = 0
14195
try:
142-
port_data = port.portrange(data['host'], data['timeout'], data['startport'], data['endport'])
143-
logging.debug("Running sensor: %s" % port.get_kind())
96+
logging.debug("Running sensor: %s" % nmap.get_kind())
97+
if '/' in data['ip']:
98+
validCIDR = nmap.validateCIDRBlock(data['ip'])
99+
if validCIDR:
100+
tmpMessage = "True"
101+
cidr = nmap.returnCIDR(data['ip'])
102+
for ip in islice(cidr, 1, len(cidr)-1):
103+
result = nmap.do_one_ping(ip, float(data['timeout'])/1000)
104+
if not result == None:
105+
alive_str = alive_str + ip + ": " + str(int(result * 1000)) + " ms, "
106+
alive_cnt = alive_cnt + 1
107+
else:
108+
tmpMessage = validCIDR
109+
error = True
110+
channel_list = [
111+
{
112+
"name": "Hosts alive",
113+
"mode": "Integer",
114+
"kind": "count",
115+
"value": alive_cnt
116+
}]
144117
except Exception as e:
145-
logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (port.get_kind(),
118+
logging.error("Ooops Something went wrong with '%s' sensor %s. Error: %s" % (nmap.get_kind(),
146119
data['sensorid'], e))
147120
sensor_data = {
148121
"sensorid": int(data['sensorid']),
149122
"error": "Exception",
150123
"code": 1,
151124
"message": "Port check failed or ports closed. See log for details"
152125
}
153-
return sensor_data
126+
out_queue.put(sensor_data)
127+
return
154128
sensor_data = {
155129
"sensorid": int(data['sensorid']),
156-
"message": "OK Ports open",
157-
"channel": port_data
130+
"message": alive_str[:-2],
131+
"channel": channel_list
158132
}
159-
del port
133+
del nmap
160134
gc.collect()
161-
return sensor_data
135+
out_queue.put(sensor_data)
136+
137+
def ip2bin(self,ip):
138+
b = ""
139+
nmap = NMAP()
140+
inQuads = ip.split(".")
141+
outQuads = 4
142+
for q in inQuads:
143+
if q != "":
144+
b += nmap.dec2bin(int(q),8)
145+
outQuads -= 1
146+
while outQuads > 0:
147+
b += "00000000"
148+
outQuads -= 1
149+
return b
150+
151+
def dec2bin(self,n,d=None):
152+
s = ""
153+
while n>0:
154+
if n&1:
155+
s = "1"+s
156+
else:
157+
s = "0"+s
158+
n >>= 1
159+
if d is not None:
160+
while len(s)<d:
161+
s = "0"+s
162+
if s == "": s = "0"
163+
return s
164+
165+
def bin2ip(self,b):
166+
ip = ""
167+
for i in range(0,len(b),8):
168+
ip += str(int(b[i:i+8],2))+"."
169+
return ip[:-1]
170+
171+
def validateCIDRBlock(self,b):
172+
# appropriate format for CIDR block ($prefix/$subnet)
173+
p = re.compile("^([0-9]{1,3}\.){0,3}[0-9]{1,3}(/[0-9]{1,2}){1}$")
174+
if not p.match(b):
175+
return "Error: Invalid CIDR format!"
176+
# extract prefix and subnet size
177+
prefix, subnet = b.split("/")
178+
# each quad has an appropriate value (1-255)
179+
quads = prefix.split(".")
180+
for q in quads:
181+
if (int(q) < 0) or (int(q) > 255):
182+
return "Error: quad "+str(q)+" wrong size."
183+
# subnet is an appropriate value (1-32)
184+
if (int(subnet) < 1) or (int(subnet) > 32):
185+
return "Error: subnet "+str(subnet)+" wrong size."
186+
# passed all checks -> return True
187+
return True
188+
189+
def returnCIDR(self,c):
190+
nmap = NMAP()
191+
ips = []
192+
parts = c.split("/")
193+
baseIP = nmap.ip2bin(parts[0])
194+
subnet = int(parts[1])
195+
# Python string-slicing weirdness:
196+
# "myString"[:-1] -> "myStrin" but "myString"[:0] -> ""
197+
# if a subnet of 32 was specified simply print the single IP
198+
if subnet == 32:
199+
return nmap.bin2ip(baseIP)
200+
# for any other size subnet, print a list of IP addresses by concatenating
201+
# the prefix with each of the suffixes in the subnet
202+
else:
203+
ipPrefix = baseIP[:-(32-subnet)]
204+
for i in range(2**(32-subnet)):
205+
ips.append(nmap.bin2ip(ipPrefix+nmap.dec2bin(i, (32-subnet))))
206+
return ips
207+
208+
def checksum(self, source_string):
209+
"""
210+
I'm not too confident that this is right but testing seems
211+
to suggest that it gives the same answers as in_cksum in ping.c
212+
"""
213+
sum = 0
214+
countTo = (len(source_string)/2)*2
215+
count = 0
216+
while count<countTo:
217+
thisVal = ord(source_string[count + 1])*256 + ord(source_string[count])
218+
sum = sum + thisVal
219+
sum = sum & 0xffffffff # Necessary?
220+
count = count + 2
221+
if countTo<len(source_string):
222+
sum = sum + ord(source_string[len(source_string) - 1])
223+
sum = sum & 0xffffffff # Necessary?
224+
sum = (sum >> 16) + (sum & 0xffff)
225+
sum = sum + (sum >> 16)
226+
answer = ~sum
227+
answer = answer & 0xffff
228+
# Swap bytes. Bugger me if I know why.
229+
answer = answer >> 8 | (answer << 8 & 0xff00)
230+
return answer
231+
232+
def receive_one_ping(self, my_socket, ID, timeout):
233+
"""
234+
receive the ping from the socket.
235+
"""
236+
timeLeft = float(timeout)
237+
while True:
238+
startedSelect = time.time()
239+
whatReady = select.select([my_socket], [], [], timeLeft)
240+
howLongInSelect = (time.time() - startedSelect)
241+
if whatReady[0] == []: # Timeout
242+
return
243+
timeReceived = time.time()
244+
recPacket, addr = my_socket.recvfrom(1024)
245+
icmpHeader = recPacket[20:28]
246+
type, code, checksum, packetID, sequence = struct.unpack(
247+
"bbHHh", icmpHeader
248+
)
249+
if packetID == ID:
250+
bytesInDouble = struct.calcsize("d")
251+
timeSent = struct.unpack("d", recPacket[28:28 + bytesInDouble])[0]
252+
return timeReceived - timeSent
253+
timeLeft = timeLeft - howLongInSelect
254+
if timeLeft <= 0:
255+
return
256+
257+
def send_one_ping(self, my_socket, dest_addr, ID):
258+
"""
259+
Send one ping to the given >dest_addr<.
260+
"""
261+
dest_addr = socket.gethostbyname(dest_addr)
262+
# Header is type (8), code (8), checksum (16), id (16), sequence (16)
263+
my_checksum = 0
264+
# Make a dummy heder with a 0 checksum.
265+
header = struct.pack("bbHHh", self.ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1)
266+
bytesInDouble = struct.calcsize("d")
267+
data = (192 - bytesInDouble) * "Q"
268+
data = struct.pack("d", time.time()) + data
269+
# Calculate the checksum on the data and the dummy header.
270+
my_checksum = self.checksum(header + data)
271+
# Now that we have the right checksum, we put that in. It's just easier
272+
# to make up a new header than to stuff it into the dummy.
273+
header = struct.pack(
274+
"bbHHh", self.ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1
275+
)
276+
packet = header + data
277+
my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1
278+
279+
def do_one_ping(self, dest_addr, timeout):
280+
"""
281+
Returns either the delay (in seconds) or none on timeout.
282+
"""
283+
icmp = socket.getprotobyname("icmp")
284+
try:
285+
my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
286+
except socket.error, (errno, msg):
287+
if errno == 1:
288+
# Operation not permitted
289+
msg = msg + (
290+
" - Note that ICMP messages can only be sent from processes"
291+
" running as root."
292+
)
293+
raise socket.error(msg)
294+
raise # raise the original error
295+
my_ID = os.getpid() & 0xFFFF
296+
self.send_one_ping(my_socket, dest_addr, my_ID)
297+
delay = self.receive_one_ping(my_socket, my_ID, timeout)
298+
my_socket.close()
299+
return delay

0 commit comments

Comments
 (0)