1 # -*- encoding: utf-8 -*-
2 # Samba traffic replay and learning
4 # Copyright (C) Catalyst IT Ltd. 2017
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 from __future__ import print_function, division
30 from collections import OrderedDict, Counter, defaultdict
31 from samba.emulate import traffic_packets
32 from samba.samdb import SamDB
34 from ldb import LdbError
35 from samba.dcerpc import ClientConnection
36 from samba.dcerpc import security, drsuapi, lsa
37 from samba.dcerpc import netlogon
38 from samba.dcerpc.netlogon import netr_Authenticator
39 from samba.dcerpc import srvsvc
40 from samba.dcerpc import samr
41 from samba.drs_utils import drs_DsBind
43 from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS
44 from samba.auth import system_session
45 from samba.dsdb import (
47 UF_SERVER_TRUST_ACCOUNT,
48 UF_TRUSTED_FOR_DELEGATION
50 from samba.dcerpc.misc import SEC_CHAN_BDC
51 from samba import gensec
52 from samba import sd_utils
53 from samba.compat import get_string
54 from samba.logger import get_samba_logger
58 # we don't use None, because it complicates [de]serialisation
62 ('dns', '0'): 1.0, # query
63 ('smb', '0x72'): 1.0, # Negotiate protocol
64 ('ldap', '0'): 1.0, # bind
65 ('ldap', '3'): 1.0, # searchRequest
66 ('ldap', '2'): 1.0, # unbindRequest
68 ('dcerpc', '11'): 1.0, # bind
69 ('dcerpc', '14'): 1.0, # Alter_context
70 ('nbns', '0'): 1.0, # query
74 ('dns', '1'): 1.0, # response
75 ('ldap', '1'): 1.0, # bind response
76 ('ldap', '4'): 1.0, # search result
77 ('ldap', '5'): 1.0, # search done
79 ('dcerpc', '12'): 1.0, # bind_ack
80 ('dcerpc', '13'): 1.0, # bind_nak
81 ('dcerpc', '15'): 1.0, # Alter_context response
84 SKIPPED_PROTOCOLS = {"smb", "smb2", "browser", "smb_netlogon"}
87 WAIT_THRESHOLD = (1.0 / WAIT_SCALE)
88 NO_WAIT_LOG_TIME_RANGE = (-10, -3)
90 # DEBUG_LEVEL can be changed by scripts with -d
93 LOGGER = get_samba_logger(name=__name__)
96 def debug(level, msg, *args):
97 """Print a formatted debug message to standard error.
100 :param level: The debug level, message will be printed if it is <= the
101 currently set debug level. The debug level can be set with
103 :param msg: The message to be logged, can contain C-Style format
105 :param args: The parameters required by the format specifiers
107 if level <= DEBUG_LEVEL:
109 print(msg, file=sys.stderr)
111 print(msg % tuple(args), file=sys.stderr)
114 def debug_lineno(*args):
115 """ Print an unformatted log message to stderr, contaning the line number
117 tb = traceback.extract_stack(limit=2)
118 print((" %s:" "\033[01;33m"
119 "%s " "\033[00m" % (tb[0][2], tb[0][1])), end=' ',
122 print(a, file=sys.stderr)
123 print(file=sys.stderr)
127 def random_colour_print():
128 """Return a function that prints a randomly coloured line to stderr"""
129 n = 18 + random.randrange(214)
130 prefix = "\033[38;5;%dm" % n
134 print("%s%s\033[00m" % (prefix, a), file=sys.stderr)
139 class FakePacketError(Exception):
143 class Packet(object):
144 """Details of a network packet"""
145 def __init__(self, timestamp, ip_protocol, stream_number, src, dest,
146 protocol, opcode, desc, extra):
148 self.timestamp = timestamp
149 self.ip_protocol = ip_protocol
150 self.stream_number = stream_number
153 self.protocol = protocol
157 if self.src < self.dest:
158 self.endpoints = (self.src, self.dest)
160 self.endpoints = (self.dest, self.src)
163 def from_line(self, line):
164 fields = line.rstrip('\n').split('\t')
175 timestamp = float(timestamp)
179 return Packet(timestamp, ip_protocol, stream_number, src, dest,
180 protocol, opcode, desc, extra)
182 def as_summary(self, time_offset=0.0):
183 """Format the packet as a traffic_summary line.
185 extra = '\t'.join(self.extra)
186 t = self.timestamp + time_offset
187 return (t, '%f\t%s\t%s\t%d\t%d\t%s\t%s\t%s\t%s' %
190 self.stream_number or '',
199 return ("%.3f: %d -> %d; ip %s; strm %s; prot %s; op %s; desc %s %s" %
200 (self.timestamp, self.src, self.dest, self.ip_protocol or '-',
201 self.stream_number, self.protocol, self.opcode, self.desc,
202 ('«' + ' '.join(self.extra) + '»' if self.extra else '')))
205 return "<Packet @%s>" % self
208 return self.__class__(self.timestamp,
218 def as_packet_type(self):
219 t = '%s:%s' % (self.protocol, self.opcode)
222 def client_score(self):
223 """A positive number means we think it is a client; a negative number
224 means we think it is a server. Zero means no idea. range: -1 to 1.
226 key = (self.protocol, self.opcode)
227 if key in CLIENT_CLUES:
228 return CLIENT_CLUES[key]
229 if key in SERVER_CLUES:
230 return -SERVER_CLUES[key]
233 def play(self, conversation, context):
234 """Send the packet over the network, if required.
236 Some packets are ignored, i.e. for protocols not handled,
237 server response messages, or messages that are generated by the
238 protocol layer associated with other packets.
240 fn_name = 'packet_%s_%s' % (self.protocol, self.opcode)
242 fn = getattr(traffic_packets, fn_name)
244 except AttributeError as e:
245 print("Conversation(%s) Missing handler %s" %
246 (conversation.conversation_id, fn_name),
250 # Don't display a message for kerberos packets, they're not directly
251 # generated they're used to indicate kerberos should be used
252 if self.protocol != "kerberos":
253 debug(2, "Conversation(%s) Calling handler %s" %
254 (conversation.conversation_id, fn_name))
258 if fn(self, conversation, context):
259 # Only collect timing data for functions that generate
260 # network traffic, or fail
262 duration = end - start
263 print("%f\t%s\t%s\t%s\t%f\tTrue\t" %
264 (end, conversation.conversation_id, self.protocol,
265 self.opcode, duration))
266 except Exception as e:
268 duration = end - start
269 print("%f\t%s\t%s\t%s\t%f\tFalse\t%s" %
270 (end, conversation.conversation_id, self.protocol,
271 self.opcode, duration, e))
273 def __cmp__(self, other):
274 return self.timestamp - other.timestamp
276 def is_really_a_packet(self, missing_packet_stats=None):
277 """Is the packet one that can be ignored?
279 If so removing it will have no effect on the replay
281 if self.protocol in SKIPPED_PROTOCOLS:
282 # Ignore any packets for the protocols we're not interested in.
284 if self.protocol == "ldap" and self.opcode == '':
285 # skip ldap continuation packets
288 fn_name = 'packet_%s_%s' % (self.protocol, self.opcode)
289 fn = getattr(traffic_packets, fn_name, None)
291 print("missing packet %s" % fn_name, file=sys.stderr)
293 if fn is traffic_packets.null_packet:
298 class ReplayContext(object):
299 """State/Context for an individual conversation between an simulated client
307 badpassword_frequency=None,
308 prefer_kerberos=None,
317 self.ldap_connections = []
318 self.dcerpc_connections = []
319 self.lsarpc_connections = []
320 self.lsarpc_connections_named = []
321 self.drsuapi_connections = []
322 self.srvsvc_connections = []
323 self.samr_contexts = []
324 self.netlogon_connection = None
327 self.prefer_kerberos = prefer_kerberos
329 self.base_dn = base_dn
331 self.statsdir = statsdir
332 self.global_tempdir = tempdir
333 self.domain_sid = domain_sid
334 self.realm = lp.get('realm')
336 # Bad password attempt controls
337 self.badpassword_frequency = badpassword_frequency
338 self.last_lsarpc_bad = False
339 self.last_lsarpc_named_bad = False
340 self.last_simple_bind_bad = False
341 self.last_bind_bad = False
342 self.last_srvsvc_bad = False
343 self.last_drsuapi_bad = False
344 self.last_netlogon_bad = False
345 self.last_samlogon_bad = False
346 self.generate_ldap_search_tables()
347 self.next_conversation_id = itertools.count()
349 def generate_ldap_search_tables(self):
350 session = system_session()
352 db = SamDB(url="ldap://%s" % self.server,
353 session_info=session,
354 credentials=self.creds,
357 res = db.search(db.domain_dn(),
358 scope=ldb.SCOPE_SUBTREE,
359 controls=["paged_results:1:1000"],
362 # find a list of dns for each pattern
363 # e.g. CN,CN,CN,DC,DC
365 attribute_clue_map = {
371 pattern = ','.join(x.lstrip()[:2] for x in dn.split(',')).upper()
372 dns = dn_map.setdefault(pattern, [])
374 if dn.startswith('CN=NTDS Settings,'):
375 attribute_clue_map['invocationId'].append(dn)
377 # extend the map in case we are working with a different
378 # number of DC components.
379 # for k, v in self.dn_map.items():
380 # print >>sys.stderr, k, len(v)
382 for k in list(dn_map.keys()):
386 while p[-3:] == ',DC':
390 if p != k and p in dn_map:
391 print('dn_map collison %s %s' % (k, p),
394 dn_map[p] = dn_map[k]
397 self.attribute_clue_map = attribute_clue_map
399 def generate_process_local_config(self, account, conversation):
402 self.netbios_name = account.netbios_name
403 self.machinepass = account.machinepass
404 self.username = account.username
405 self.userpass = account.userpass
407 self.tempdir = mk_masked_dir(self.global_tempdir,
409 conversation.conversation_id)
411 self.lp.set("private dir", self.tempdir)
412 self.lp.set("lock dir", self.tempdir)
413 self.lp.set("state directory", self.tempdir)
414 self.lp.set("tls verify peer", "no_check")
416 # If the domain was not specified, check for the environment
418 if self.domain is None:
419 self.domain = os.environ["DOMAIN"]
421 self.remoteAddress = "/root/ncalrpc_as_system"
422 self.samlogon_dn = ("cn=%s,%s" %
423 (self.netbios_name, self.ou))
424 self.user_dn = ("cn=%s,%s" %
425 (self.username, self.ou))
427 self.generate_machine_creds()
428 self.generate_user_creds()
430 def with_random_bad_credentials(self, f, good, bad, failed_last_time):
431 """Execute the supplied logon function, randomly choosing the
434 Based on the frequency in badpassword_frequency randomly perform the
435 function with the supplied bad credentials.
436 If run with bad credentials, the function is re-run with the good
438 failed_last_time is used to prevent consecutive bad credential
439 attempts. So the over all bad credential frequency will be lower
440 than that requested, but not significantly.
442 if not failed_last_time:
443 if (self.badpassword_frequency and self.badpassword_frequency > 0
444 and random.random() < self.badpassword_frequency):
448 # Ignore any exceptions as the operation may fail
449 # as it's being performed with bad credentials
451 failed_last_time = True
453 failed_last_time = False
456 return (result, failed_last_time)
458 def generate_user_creds(self):
459 """Generate the conversation specific user Credentials.
461 Each Conversation has an associated user account used to simulate
462 any non Administrative user traffic.
464 Generates user credentials with good and bad passwords and ldap
465 simple bind credentials with good and bad passwords.
467 self.user_creds = Credentials()
468 self.user_creds.guess(self.lp)
469 self.user_creds.set_workstation(self.netbios_name)
470 self.user_creds.set_password(self.userpass)
471 self.user_creds.set_username(self.username)
472 self.user_creds.set_domain(self.domain)
473 if self.prefer_kerberos:
474 self.user_creds.set_kerberos_state(MUST_USE_KERBEROS)
476 self.user_creds.set_kerberos_state(DONT_USE_KERBEROS)
478 self.user_creds_bad = Credentials()
479 self.user_creds_bad.guess(self.lp)
480 self.user_creds_bad.set_workstation(self.netbios_name)
481 self.user_creds_bad.set_password(self.userpass[:-4])
482 self.user_creds_bad.set_username(self.username)
483 if self.prefer_kerberos:
484 self.user_creds_bad.set_kerberos_state(MUST_USE_KERBEROS)
486 self.user_creds_bad.set_kerberos_state(DONT_USE_KERBEROS)
488 # Credentials for ldap simple bind.
489 self.simple_bind_creds = Credentials()
490 self.simple_bind_creds.guess(self.lp)
491 self.simple_bind_creds.set_workstation(self.netbios_name)
492 self.simple_bind_creds.set_password(self.userpass)
493 self.simple_bind_creds.set_username(self.username)
494 self.simple_bind_creds.set_gensec_features(
495 self.simple_bind_creds.get_gensec_features() | gensec.FEATURE_SEAL)
496 if self.prefer_kerberos:
497 self.simple_bind_creds.set_kerberos_state(MUST_USE_KERBEROS)
499 self.simple_bind_creds.set_kerberos_state(DONT_USE_KERBEROS)
500 self.simple_bind_creds.set_bind_dn(self.user_dn)
502 self.simple_bind_creds_bad = Credentials()
503 self.simple_bind_creds_bad.guess(self.lp)
504 self.simple_bind_creds_bad.set_workstation(self.netbios_name)
505 self.simple_bind_creds_bad.set_password(self.userpass[:-4])
506 self.simple_bind_creds_bad.set_username(self.username)
507 self.simple_bind_creds_bad.set_gensec_features(
508 self.simple_bind_creds_bad.get_gensec_features() |
510 if self.prefer_kerberos:
511 self.simple_bind_creds_bad.set_kerberos_state(MUST_USE_KERBEROS)
513 self.simple_bind_creds_bad.set_kerberos_state(DONT_USE_KERBEROS)
514 self.simple_bind_creds_bad.set_bind_dn(self.user_dn)
516 def generate_machine_creds(self):
517 """Generate the conversation specific machine Credentials.
519 Each Conversation has an associated machine account.
521 Generates machine credentials with good and bad passwords.
524 self.machine_creds = Credentials()
525 self.machine_creds.guess(self.lp)
526 self.machine_creds.set_workstation(self.netbios_name)
527 self.machine_creds.set_secure_channel_type(SEC_CHAN_BDC)
528 self.machine_creds.set_password(self.machinepass)
529 self.machine_creds.set_username(self.netbios_name + "$")
530 self.machine_creds.set_domain(self.domain)
531 if self.prefer_kerberos:
532 self.machine_creds.set_kerberos_state(MUST_USE_KERBEROS)
534 self.machine_creds.set_kerberos_state(DONT_USE_KERBEROS)
536 self.machine_creds_bad = Credentials()
537 self.machine_creds_bad.guess(self.lp)
538 self.machine_creds_bad.set_workstation(self.netbios_name)
539 self.machine_creds_bad.set_secure_channel_type(SEC_CHAN_BDC)
540 self.machine_creds_bad.set_password(self.machinepass[:-4])
541 self.machine_creds_bad.set_username(self.netbios_name + "$")
542 if self.prefer_kerberos:
543 self.machine_creds_bad.set_kerberos_state(MUST_USE_KERBEROS)
545 self.machine_creds_bad.set_kerberos_state(DONT_USE_KERBEROS)
547 def get_matching_dn(self, pattern, attributes=None):
548 # If the pattern is an empty string, we assume ROOTDSE,
549 # Otherwise we try adding or removing DC suffixes, then
550 # shorter leading patterns until we hit one.
551 # e.g if there is no CN,CN,CN,CN,DC,DC
552 # we first try CN,CN,CN,CN,DC
553 # and CN,CN,CN,CN,DC,DC,DC
554 # then change to CN,CN,CN,DC,DC
555 # and as last resort we use the base_dn
556 attr_clue = self.attribute_clue_map.get(attributes)
558 return random.choice(attr_clue)
560 pattern = pattern.upper()
562 if pattern in self.dn_map:
563 return random.choice(self.dn_map[pattern])
564 # chop one off the front and try it all again.
565 pattern = pattern[3:]
569 def get_dcerpc_connection(self, new=False):
570 guid = '12345678-1234-abcd-ef00-01234567cffb' # RPC_NETLOGON UUID
571 if self.dcerpc_connections and not new:
572 return self.dcerpc_connections[-1]
573 c = ClientConnection("ncacn_ip_tcp:%s" % self.server,
575 self.dcerpc_connections.append(c)
578 def get_srvsvc_connection(self, new=False):
579 if self.srvsvc_connections and not new:
580 return self.srvsvc_connections[-1]
583 return srvsvc.srvsvc("ncacn_np:%s" % (self.server),
587 (c, self.last_srvsvc_bad) = \
588 self.with_random_bad_credentials(connect,
591 self.last_srvsvc_bad)
593 self.srvsvc_connections.append(c)
596 def get_lsarpc_connection(self, new=False):
597 if self.lsarpc_connections and not new:
598 return self.lsarpc_connections[-1]
601 binding_options = 'schannel,seal,sign'
602 return lsa.lsarpc("ncacn_ip_tcp:%s[%s]" %
603 (self.server, binding_options),
607 (c, self.last_lsarpc_bad) = \
608 self.with_random_bad_credentials(connect,
610 self.machine_creds_bad,
611 self.last_lsarpc_bad)
613 self.lsarpc_connections.append(c)
616 def get_lsarpc_named_pipe_connection(self, new=False):
617 if self.lsarpc_connections_named and not new:
618 return self.lsarpc_connections_named[-1]
621 return lsa.lsarpc("ncacn_np:%s" % (self.server),
625 (c, self.last_lsarpc_named_bad) = \
626 self.with_random_bad_credentials(connect,
628 self.machine_creds_bad,
629 self.last_lsarpc_named_bad)
631 self.lsarpc_connections_named.append(c)
634 def get_drsuapi_connection_pair(self, new=False, unbind=False):
635 """get a (drs, drs_handle) tuple"""
636 if self.drsuapi_connections and not new:
637 c = self.drsuapi_connections[-1]
641 binding_options = 'seal'
642 binding_string = "ncacn_ip_tcp:%s[%s]" %\
643 (self.server, binding_options)
644 return drsuapi.drsuapi(binding_string, self.lp, creds)
646 (drs, self.last_drsuapi_bad) = \
647 self.with_random_bad_credentials(connect,
650 self.last_drsuapi_bad)
652 (drs_handle, supported_extensions) = drs_DsBind(drs)
653 c = (drs, drs_handle)
654 self.drsuapi_connections.append(c)
657 def get_ldap_connection(self, new=False, simple=False):
658 if self.ldap_connections and not new:
659 return self.ldap_connections[-1]
661 def simple_bind(creds):
663 To run simple bind against Windows, we need to run
664 following commands in PowerShell:
666 Install-windowsfeature ADCS-Cert-Authority
667 Install-AdcsCertificationAuthority -CAType EnterpriseRootCA
671 return SamDB('ldaps://%s' % self.server,
675 def sasl_bind(creds):
676 return SamDB('ldap://%s' % self.server,
680 (samdb, self.last_simple_bind_bad) = \
681 self.with_random_bad_credentials(simple_bind,
682 self.simple_bind_creds,
683 self.simple_bind_creds_bad,
684 self.last_simple_bind_bad)
686 (samdb, self.last_bind_bad) = \
687 self.with_random_bad_credentials(sasl_bind,
692 self.ldap_connections.append(samdb)
695 def get_samr_context(self, new=False):
696 if not self.samr_contexts or new:
697 self.samr_contexts.append(
698 SamrContext(self.server, lp=self.lp, creds=self.creds))
699 return self.samr_contexts[-1]
701 def get_netlogon_connection(self):
703 if self.netlogon_connection:
704 return self.netlogon_connection
707 return netlogon.netlogon("ncacn_ip_tcp:%s[schannel,seal]" %
711 (c, self.last_netlogon_bad) = \
712 self.with_random_bad_credentials(connect,
714 self.machine_creds_bad,
715 self.last_netlogon_bad)
716 self.netlogon_connection = c
719 def guess_a_dns_lookup(self):
720 return (self.realm, 'A')
722 def get_authenticator(self):
723 auth = self.machine_creds.new_client_authenticator()
724 current = netr_Authenticator()
725 current.cred.data = [x if isinstance(x, int) else ord(x) for x in auth["credential"]]
726 current.timestamp = auth["timestamp"]
728 subsequent = netr_Authenticator()
729 return (current, subsequent)
732 class SamrContext(object):
733 """State/Context associated with a samr connection.
735 def __init__(self, server, lp=None, creds=None):
736 self.connection = None
738 self.domain_handle = None
739 self.domain_sid = None
740 self.group_handle = None
741 self.user_handle = None
747 def get_connection(self):
748 if not self.connection:
749 self.connection = samr.samr(
750 "ncacn_ip_tcp:%s[seal]" % (self.server),
752 credentials=self.creds)
754 return self.connection
756 def get_handle(self):
758 c = self.get_connection()
759 self.handle = c.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
763 class Conversation(object):
764 """Details of a converation between a simulated client and a server."""
765 conversation_id = None
767 def __init__(self, start_time=None, endpoints=None):
768 self.start_time = start_time
769 self.endpoints = endpoints
771 self.msg = random_colour_print()
772 self.client_balance = 0.0
774 def __cmp__(self, other):
775 if self.start_time is None:
776 if other.start_time is None:
779 if other.start_time is None:
781 return self.start_time - other.start_time
783 def add_packet(self, packet):
784 """Add a packet object to this conversation, making a local copy with
785 a conversation-relative timestamp."""
788 if self.start_time is None:
789 self.start_time = p.timestamp
791 if self.endpoints is None:
792 self.endpoints = p.endpoints
794 if p.endpoints != self.endpoints:
795 raise FakePacketError("Conversation endpoints %s don't match"
796 "packet endpoints %s" %
797 (self.endpoints, p.endpoints))
799 p.timestamp -= self.start_time
801 if p.src == p.endpoints[0]:
802 self.client_balance -= p.client_score()
804 self.client_balance += p.client_score()
806 if p.is_really_a_packet():
807 self.packets.append(p)
809 def add_short_packet(self, timestamp, protocol, opcode, extra,
811 """Create a packet from a timestamp, and 'protocol:opcode' pair, and a
812 (possibly empty) list of extra data. If client is True, assume
813 this packet is from the client to the server.
815 src, dest = self.guess_client_server()
817 src, dest = dest, src
818 key = (protocol, opcode)
819 desc = OP_DESCRIPTIONS[key] if key in OP_DESCRIPTIONS else ''
820 if protocol in IP_PROTOCOLS:
821 ip_protocol = IP_PROTOCOLS[protocol]
824 packet = Packet(timestamp - self.start_time, ip_protocol,
826 protocol, opcode, desc, extra)
827 # XXX we're assuming the timestamp is already adjusted for
829 # XXX should we adjust client balance for guessed packets?
830 if packet.src == packet.endpoints[0]:
831 self.client_balance -= packet.client_score()
833 self.client_balance += packet.client_score()
834 if packet.is_really_a_packet():
835 self.packets.append(packet)
838 return ("<Conversation %s %s starting %.3f %d packets>" %
839 (self.conversation_id, self.endpoints, self.start_time,
845 return iter(self.packets)
848 return len(self.packets)
850 def get_duration(self):
851 if len(self.packets) < 2:
853 return self.packets[-1].timestamp - self.packets[0].timestamp
855 def replay_as_summary_lines(self):
857 for p in self.packets:
858 lines.append(p.as_summary(self.start_time))
861 def replay_in_fork_with_delay(self, start, context=None, account=None):
862 """Fork a new process and replay the conversation.
864 def signal_handler(signal, frame):
865 """Signal handler closes standard out and error.
867 Triggered by a sigterm, ensures that the log messages are flushed
868 to disk and not lost.
875 now = time.time() - start
877 # we are replaying strictly in order, so it is safe to sleep
878 # in the main process if the gap is big enough. This reduces
879 # the number of concurrent threads, which allows us to make
881 if gap > 0.15 and False:
882 print("sleeping for %f in main process" % (gap - 0.1),
884 time.sleep(gap - 0.1)
885 now = time.time() - start
887 print("gap is now %f" % gap, file=sys.stderr)
889 self.conversation_id = next(context.next_conversation_id)
894 signal.signal(signal.SIGTERM, signal_handler)
895 # we must never return, or we'll end up running parts of the
896 # parent's clean-up code. So we work in a try...finally, and
897 # try to print any exceptions.
900 context.generate_process_local_config(account, self)
903 filename = os.path.join(context.statsdir, 'stats-conversation-%d' %
904 self.conversation_id)
906 sys.stdout = open(filename, 'w')
908 sleep_time = gap - SLEEP_OVERHEAD
910 time.sleep(sleep_time)
912 miss = t - (time.time() - start)
913 self.msg("starting %s [miss %.3f pid %d]" % (self, miss, pid))
916 print(("EXCEPTION in child PID %d, conversation %s" % (pid, self)),
918 traceback.print_exc(sys.stderr)
924 def replay(self, context=None):
927 for p in self.packets:
928 now = time.time() - start
929 gap = p.timestamp - now
930 sleep_time = gap - SLEEP_OVERHEAD
932 time.sleep(sleep_time)
934 miss = p.timestamp - (time.time() - start)
936 self.msg("packet %s [miss %.3f pid %d]" % (p, miss,
939 p.play(self, context)
941 def guess_client_server(self, server_clue=None):
942 """Have a go at deciding who is the server and who is the client.
943 returns (client, server)
945 a, b = self.endpoints
947 if self.client_balance < 0:
950 # in the absense of a clue, we will fall through to assuming
951 # the lowest number is the server (which is usually true).
953 if self.client_balance == 0 and server_clue == b:
958 def forget_packets_outside_window(self, s, e):
959 """Prune any packets outside the timne window we're interested in
961 :param s: start of the window
962 :param e: end of the window
964 self.packets = [p for p in self.packets if s <= p.timestamp <= e]
965 self.start_time = self.packets[0].timestamp if self.packets else None
967 def renormalise_times(self, start_time):
968 """Adjust the packet start times relative to the new start time."""
969 for p in self.packets:
970 p.timestamp -= start_time
972 if self.start_time is not None:
973 self.start_time -= start_time
976 class DnsHammer(Conversation):
977 """A lightweight conversation that generates a lot of dns:0 packets on
980 def __init__(self, dns_rate, duration):
981 n = int(dns_rate * duration)
982 self.times = [random.uniform(0, duration) for i in range(n)]
985 self.duration = duration
987 self.msg = random_colour_print()
990 return ("<DnsHammer %d packets over %.1fs (rate %.2f)>" %
991 (len(self.times), self.duration, self.rate))
993 def replay_in_fork_with_delay(self, start, context=None, account=None):
994 return Conversation.replay_in_fork_with_delay(self,
999 def replay(self, context=None):
1001 fn = traffic_packets.packet_dns_0
1002 for t in self.times:
1003 now = time.time() - start
1005 sleep_time = gap - SLEEP_OVERHEAD
1007 time.sleep(sleep_time)
1010 miss = t - (time.time() - start)
1011 self.msg("packet %s [miss %.3f pid %d]" % (t, miss,
1015 packet_start = time.time()
1017 fn(self, self, context)
1019 duration = end - packet_start
1020 print("%f\tDNS\tdns\t0\t%f\tTrue\t" % (end, duration))
1021 except Exception as e:
1023 duration = end - packet_start
1024 print("%f\tDNS\tdns\t0\t%f\tFalse\t%s" % (end, duration, e))
1027 def ingest_summaries(files, dns_mode='count'):
1028 """Load a summary traffic summary file and generated Converations from it.
1031 dns_counts = defaultdict(int)
1034 if isinstance(f, str):
1036 print("Ingesting %s" % (f.name,), file=sys.stderr)
1038 p = Packet.from_line(line)
1039 if p.protocol == 'dns' and dns_mode != 'include':
1040 dns_counts[p.opcode] += 1
1049 start_time = min(p.timestamp for p in packets)
1050 last_packet = max(p.timestamp for p in packets)
1052 print("gathering packets into conversations", file=sys.stderr)
1053 conversations = OrderedDict()
1055 p.timestamp -= start_time
1056 c = conversations.get(p.endpoints)
1059 conversations[p.endpoints] = c
1062 # We only care about conversations with actual traffic, so we
1063 # filter out conversations with nothing to say. We do that here,
1064 # rather than earlier, because those empty packets contain useful
1065 # hints as to which end of the conversation was the client.
1066 conversation_list = []
1067 for c in conversations.values():
1069 conversation_list.append(c)
1071 # This is obviously not correct, as many conversations will appear
1072 # to start roughly simultaneously at the beginning of the snapshot.
1073 # To which we say: oh well, so be it.
1074 duration = float(last_packet - start_time)
1075 mean_interval = len(conversations) / duration
1077 return conversation_list, mean_interval, duration, dns_counts
1080 def guess_server_address(conversations):
1081 # we guess the most common address.
1082 addresses = Counter()
1083 for c in conversations:
1084 addresses.update(c.endpoints)
1086 return addresses.most_common(1)[0]
1089 def stringify_keys(x):
1091 for k, v in x.items():
1097 def unstringify_keys(x):
1099 for k, v in x.items():
1100 t = tuple(str(k).split('\t'))
1105 class TrafficModel(object):
1106 def __init__(self, n=3):
1108 self.query_details = {}
1110 self.dns_opcounts = defaultdict(int)
1111 self.cumulative_duration = 0.0
1112 self.conversation_rate = [0, 1]
1114 def learn(self, conversations, dns_opcounts={}):
1117 key = (NON_PACKET,) * (self.n - 1)
1119 server = guess_server_address(conversations)
1121 for k, v in dns_opcounts.items():
1122 self.dns_opcounts[k] += v
1124 if len(conversations) > 1:
1126 conversations[-1].start_time - conversations[0].start_time
1127 self.conversation_rate[0] = len(conversations)
1128 self.conversation_rate[1] = elapsed
1130 for c in conversations:
1131 client, server = c.guess_client_server(server)
1132 cum_duration += c.get_duration()
1133 key = (NON_PACKET,) * (self.n - 1)
1138 elapsed = p.timestamp - prev
1140 if elapsed > WAIT_THRESHOLD:
1141 # add the wait as an extra state
1142 wait = 'wait:%d' % (math.log(max(1.0,
1143 elapsed * WAIT_SCALE)))
1144 self.ngrams.setdefault(key, []).append(wait)
1145 key = key[1:] + (wait,)
1147 short_p = p.as_packet_type()
1148 self.query_details.setdefault(short_p,
1149 []).append(tuple(p.extra))
1150 self.ngrams.setdefault(key, []).append(short_p)
1151 key = key[1:] + (short_p,)
1153 self.cumulative_duration += cum_duration
1155 self.ngrams.setdefault(key, []).append(NON_PACKET)
1159 for k, v in self.ngrams.items():
1161 ngrams[k] = dict(Counter(v))
1164 for k, v in self.query_details.items():
1165 query_details[k] = dict(Counter('\t'.join(x) if x else '-'
1170 'query_details': query_details,
1171 'cumulative_duration': self.cumulative_duration,
1172 'conversation_rate': self.conversation_rate,
1174 d['dns'] = self.dns_opcounts
1176 if isinstance(f, str):
1179 json.dump(d, f, indent=2)
1182 if isinstance(f, str):
1187 for k, v in d['ngrams'].items():
1188 k = tuple(str(k).split('\t'))
1189 values = self.ngrams.setdefault(k, [])
1190 for p, count in v.items():
1191 values.extend([str(p)] * count)
1193 for k, v in d['query_details'].items():
1194 values = self.query_details.setdefault(str(k), [])
1195 for p, count in v.items():
1197 values.extend([()] * count)
1199 values.extend([tuple(str(p).split('\t'))] * count)
1202 for k, v in d['dns'].items():
1203 self.dns_opcounts[k] += v
1205 self.cumulative_duration = d['cumulative_duration']
1206 self.conversation_rate = d['conversation_rate']
1208 def construct_conversation(self, timestamp=0.0, client=2, server=1,
1209 hard_stop=None, packet_rate=1):
1210 """Construct a individual converation from the model."""
1212 c = Conversation(timestamp, (server, client))
1214 key = (NON_PACKET,) * (self.n - 1)
1216 while key in self.ngrams:
1217 p = random.choice(self.ngrams.get(key, NON_PACKET))
1220 if p in self.query_details:
1221 extra = random.choice(self.query_details[p])
1225 protocol, opcode = p.split(':', 1)
1226 if protocol == 'wait':
1227 log_wait_time = int(opcode) + random.random()
1228 wait = math.exp(log_wait_time) / (WAIT_SCALE * packet_rate)
1231 log_wait = random.uniform(*NO_WAIT_LOG_TIME_RANGE)
1232 wait = math.exp(log_wait) / packet_rate
1234 if hard_stop is not None and timestamp > hard_stop:
1236 c.add_short_packet(timestamp, protocol, opcode, extra)
1238 key = key[1:] + (p,)
1242 def generate_conversations(self, rate, duration, packet_rate=1):
1243 """Generate a list of conversations from the model."""
1245 # We run the simulation for at least ten times as long as our
1246 # desired duration, and take a section near the start.
1247 rate_n, rate_t = self.conversation_rate
1249 duration2 = max(rate_t, duration * 2)
1250 n = rate * duration2 * rate_n / rate_t
1257 start = end - duration
1259 while client < n + 2:
1260 start = random.uniform(0, duration2)
1261 c = self.construct_conversation(start,
1264 hard_stop=(duration2 * 5),
1265 packet_rate=packet_rate)
1267 c.forget_packets_outside_window(start, end)
1268 c.renormalise_times(start)
1270 conversations.append(c)
1273 print(("we have %d conversations at rate %f" %
1274 (len(conversations), rate)), file=sys.stderr)
1275 conversations.sort()
1276 return conversations
1281 'rpc_netlogon': '06',
1282 'kerberos': '06', # ratio 16248:258
1293 'smb_netlogon': '11',
1299 ('browser', '0x01'): 'Host Announcement (0x01)',
1300 ('browser', '0x02'): 'Request Announcement (0x02)',
1301 ('browser', '0x08'): 'Browser Election Request (0x08)',
1302 ('browser', '0x09'): 'Get Backup List Request (0x09)',
1303 ('browser', '0x0c'): 'Domain/Workgroup Announcement (0x0c)',
1304 ('browser', '0x0f'): 'Local Master Announcement (0x0f)',
1305 ('cldap', '3'): 'searchRequest',
1306 ('cldap', '5'): 'searchResDone',
1307 ('dcerpc', '0'): 'Request',
1308 ('dcerpc', '11'): 'Bind',
1309 ('dcerpc', '12'): 'Bind_ack',
1310 ('dcerpc', '13'): 'Bind_nak',
1311 ('dcerpc', '14'): 'Alter_context',
1312 ('dcerpc', '15'): 'Alter_context_resp',
1313 ('dcerpc', '16'): 'AUTH3',
1314 ('dcerpc', '2'): 'Response',
1315 ('dns', '0'): 'query',
1316 ('dns', '1'): 'response',
1317 ('drsuapi', '0'): 'DsBind',
1318 ('drsuapi', '12'): 'DsCrackNames',
1319 ('drsuapi', '13'): 'DsWriteAccountSpn',
1320 ('drsuapi', '1'): 'DsUnbind',
1321 ('drsuapi', '2'): 'DsReplicaSync',
1322 ('drsuapi', '3'): 'DsGetNCChanges',
1323 ('drsuapi', '4'): 'DsReplicaUpdateRefs',
1324 ('epm', '3'): 'Map',
1325 ('kerberos', ''): '',
1326 ('ldap', '0'): 'bindRequest',
1327 ('ldap', '1'): 'bindResponse',
1328 ('ldap', '2'): 'unbindRequest',
1329 ('ldap', '3'): 'searchRequest',
1330 ('ldap', '4'): 'searchResEntry',
1331 ('ldap', '5'): 'searchResDone',
1332 ('ldap', ''): '*** Unknown ***',
1333 ('lsarpc', '14'): 'lsa_LookupNames',
1334 ('lsarpc', '15'): 'lsa_LookupSids',
1335 ('lsarpc', '39'): 'lsa_QueryTrustedDomainInfoBySid',
1336 ('lsarpc', '40'): 'lsa_SetTrustedDomainInfo',
1337 ('lsarpc', '6'): 'lsa_OpenPolicy',
1338 ('lsarpc', '76'): 'lsa_LookupSids3',
1339 ('lsarpc', '77'): 'lsa_LookupNames4',
1340 ('nbns', '0'): 'query',
1341 ('nbns', '1'): 'response',
1342 ('rpc_netlogon', '21'): 'NetrLogonDummyRoutine1',
1343 ('rpc_netlogon', '26'): 'NetrServerAuthenticate3',
1344 ('rpc_netlogon', '29'): 'NetrLogonGetDomainInfo',
1345 ('rpc_netlogon', '30'): 'NetrServerPasswordSet2',
1346 ('rpc_netlogon', '39'): 'NetrLogonSamLogonEx',
1347 ('rpc_netlogon', '40'): 'DsrEnumerateDomainTrusts',
1348 ('rpc_netlogon', '45'): 'NetrLogonSamLogonWithFlags',
1349 ('rpc_netlogon', '4'): 'NetrServerReqChallenge',
1350 ('samr', '0',): 'Connect',
1351 ('samr', '16'): 'GetAliasMembership',
1352 ('samr', '17'): 'LookupNames',
1353 ('samr', '18'): 'LookupRids',
1354 ('samr', '19'): 'OpenGroup',
1355 ('samr', '1'): 'Close',
1356 ('samr', '25'): 'QueryGroupMember',
1357 ('samr', '34'): 'OpenUser',
1358 ('samr', '36'): 'QueryUserInfo',
1359 ('samr', '39'): 'GetGroupsForUser',
1360 ('samr', '3'): 'QuerySecurity',
1361 ('samr', '5'): 'LookupDomain',
1362 ('samr', '64'): 'Connect5',
1363 ('samr', '6'): 'EnumDomains',
1364 ('samr', '7'): 'OpenDomain',
1365 ('samr', '8'): 'QueryDomainInfo',
1366 ('smb', '0x04'): 'Close (0x04)',
1367 ('smb', '0x24'): 'Locking AndX (0x24)',
1368 ('smb', '0x2e'): 'Read AndX (0x2e)',
1369 ('smb', '0x32'): 'Trans2 (0x32)',
1370 ('smb', '0x71'): 'Tree Disconnect (0x71)',
1371 ('smb', '0x72'): 'Negotiate Protocol (0x72)',
1372 ('smb', '0x73'): 'Session Setup AndX (0x73)',
1373 ('smb', '0x74'): 'Logoff AndX (0x74)',
1374 ('smb', '0x75'): 'Tree Connect AndX (0x75)',
1375 ('smb', '0xa2'): 'NT Create AndX (0xa2)',
1376 ('smb2', '0'): 'NegotiateProtocol',
1377 ('smb2', '11'): 'Ioctl',
1378 ('smb2', '14'): 'Find',
1379 ('smb2', '16'): 'GetInfo',
1380 ('smb2', '18'): 'Break',
1381 ('smb2', '1'): 'SessionSetup',
1382 ('smb2', '2'): 'SessionLogoff',
1383 ('smb2', '3'): 'TreeConnect',
1384 ('smb2', '4'): 'TreeDisconnect',
1385 ('smb2', '5'): 'Create',
1386 ('smb2', '6'): 'Close',
1387 ('smb2', '8'): 'Read',
1388 ('smb_netlogon', '0x12'): 'SAM LOGON request from client (0x12)',
1389 ('smb_netlogon', '0x17'): ('SAM Active Directory Response - '
1390 'user unknown (0x17)'),
1391 ('srvsvc', '16'): 'NetShareGetInfo',
1392 ('srvsvc', '21'): 'NetSrvGetInfo',
1396 def expand_short_packet(p, timestamp, src, dest, extra):
1397 protocol, opcode = p.split(':', 1)
1398 desc = OP_DESCRIPTIONS.get((protocol, opcode), '')
1399 ip_protocol = IP_PROTOCOLS.get(protocol, '06')
1401 line = [timestamp, ip_protocol, '', src, dest, protocol, opcode, desc]
1403 return '\t'.join(line)
1406 def replay(conversations,
1415 context = ReplayContext(server=host,
1420 if len(accounts) < len(conversations):
1421 print(("we have %d accounts but %d conversations" %
1422 (accounts, conversations)), file=sys.stderr)
1425 sorted(conversations, key=lambda x: x.start_time, reverse=True),
1428 # Set the process group so that the calling scripts are not killed
1429 # when the forked child processes are killed.
1434 if duration is None:
1435 # end 1 second after the last packet of the last conversation
1436 # to start. Conversations other than the last could still be
1437 # going, but we don't care.
1438 duration = cstack[0][0].packets[-1].timestamp + 1.0
1439 print("We will stop after %.1f seconds" % duration,
1442 end = start + duration
1444 LOGGER.info("Replaying traffic for %u conversations over %d seconds"
1445 % (len(conversations), duration))
1449 dns_hammer = DnsHammer(dns_rate, duration)
1450 cstack.append((dns_hammer, None))
1454 # we spawn a batch, wait for finishers, then spawn another
1456 batch_end = min(now + 2.0, end)
1460 c, account = cstack.pop()
1461 if c.start_time + start > batch_end:
1462 cstack.append((c, account))
1466 pid = c.replay_in_fork_with_delay(start, context, account)
1470 fork_time += elapsed
1472 print("forked %s in pid %s (in %fs)" % (c, pid,
1477 print(("forked %d times in %f seconds (avg %f)" %
1478 (fork_n, fork_time, fork_time / fork_n)),
1481 debug(2, "no forks in batch ending %f" % batch_end)
1483 while time.time() < batch_end - 1.0:
1486 pid, status = os.waitpid(-1, os.WNOHANG)
1487 except OSError as e:
1488 if e.errno != 10: # no child processes
1492 c = children.pop(pid, None)
1493 print(("process %d finished conversation %s;"
1495 (pid, c, len(children))), file=sys.stderr)
1497 if time.time() >= end:
1498 print("time to stop", file=sys.stderr)
1502 print("EXCEPTION in parent", file=sys.stderr)
1503 traceback.print_exc()
1505 for s in (15, 15, 9):
1506 print(("killing %d children with -%d" %
1507 (len(children), s)), file=sys.stderr)
1508 for pid in children:
1511 except OSError as e:
1512 if e.errno != 3: # don't fail if it has already died
1515 end = time.time() + 1
1518 pid, status = os.waitpid(-1, os.WNOHANG)
1519 except OSError as e:
1523 c = children.pop(pid, None)
1524 print(("kill -%d %d KILLED conversation %s; "
1526 (s, pid, c, len(children))),
1528 if time.time() >= end:
1536 print("%d children are missing" % len(children),
1539 # there may be stragglers that were forked just as ^C was hit
1540 # and don't appear in the list of children. We can get them
1541 # with killpg, but that will also kill us, so this is^H^H would be
1542 # goodbye, except we cheat and pretend to use ^C (SIG_INTERRUPT),
1543 # so as not to have to fuss around writing signal handlers.
1546 except KeyboardInterrupt:
1547 print("ignoring fake ^C", file=sys.stderr)
1550 def openLdb(host, creds, lp):
1551 session = system_session()
1552 ldb = SamDB(url="ldap://%s" % host,
1553 session_info=session,
1554 options=['modules:paged_searches'],
1560 def ou_name(ldb, instance_id):
1561 """Generate an ou name from the instance id"""
1562 return "ou=instance-%d,ou=traffic_replay,%s" % (instance_id,
1566 def create_ou(ldb, instance_id):
1567 """Create an ou, all created user and machine accounts will belong to it.
1569 This allows all the created resources to be cleaned up easily.
1571 ou = ou_name(ldb, instance_id)
1573 ldb.add({"dn": ou.split(',', 1)[1],
1574 "objectclass": "organizationalunit"})
1575 except LdbError as e:
1576 (status, _) = e.args
1577 # ignore already exists
1582 "objectclass": "organizationalunit"})
1583 except LdbError as e:
1584 (status, _) = e.args
1585 # ignore already exists
1591 class ConversationAccounts(object):
1592 """Details of the machine and user accounts associated with a conversation.
1594 def __init__(self, netbios_name, machinepass, username, userpass):
1595 self.netbios_name = netbios_name
1596 self.machinepass = machinepass
1597 self.username = username
1598 self.userpass = userpass
1601 def generate_replay_accounts(ldb, instance_id, number, password):
1602 """Generate a series of unique machine and user account names."""
1604 generate_traffic_accounts(ldb, instance_id, number, password)
1606 for i in range(1, number + 1):
1607 netbios_name = "STGM-%d-%d" % (instance_id, i)
1608 username = "STGU-%d-%d" % (instance_id, i)
1610 account = ConversationAccounts(netbios_name, password, username,
1612 accounts.append(account)
1616 def generate_traffic_accounts(ldb, instance_id, number, password):
1617 """Create the specified number of user and machine accounts.
1619 As accounts are not explicitly deleted between runs. This function starts
1620 with the last account and iterates backwards stopping either when it
1621 finds an already existing account or it has generated all the required
1624 print(("Generating machine and conversation accounts, "
1625 "as required for %d conversations" % number),
1628 for i in range(number, 0, -1):
1630 netbios_name = "STGM-%d-%d" % (instance_id, i)
1631 create_machine_account(ldb, instance_id, netbios_name, password)
1633 except LdbError as e:
1634 (status, _) = e.args
1640 print("Added %d new machine accounts" % added,
1644 for i in range(number, 0, -1):
1646 username = "STGU-%d-%d" % (instance_id, i)
1647 create_user_account(ldb, instance_id, username, password)
1649 except LdbError as e:
1650 (status, _) = e.args
1657 print("Added %d new user accounts" % added,
1661 def create_machine_account(ldb, instance_id, netbios_name, machinepass):
1662 """Create a machine account via ldap."""
1664 ou = ou_name(ldb, instance_id)
1665 dn = "cn=%s,%s" % (netbios_name, ou)
1666 utf16pw = ('"%s"' % get_string(machinepass)).encode('utf-16-le')
1671 "objectclass": "computer",
1672 "sAMAccountName": "%s$" % netbios_name,
1673 "userAccountControl":
1674 str(UF_TRUSTED_FOR_DELEGATION | UF_SERVER_TRUST_ACCOUNT),
1675 "unicodePwd": utf16pw})
1677 duration = end - start
1678 LOGGER.info("%f\t0\tcreate\tmachine\t%f\tTrue\t" % (end, duration))
1681 def create_user_account(ldb, instance_id, username, userpass):
1682 """Create a user account via ldap."""
1683 ou = ou_name(ldb, instance_id)
1684 user_dn = "cn=%s,%s" % (username, ou)
1685 utf16pw = ('"%s"' % get_string(userpass)).encode('utf-16-le')
1689 "objectclass": "user",
1690 "sAMAccountName": username,
1691 "userAccountControl": str(UF_NORMAL_ACCOUNT),
1692 "unicodePwd": utf16pw
1695 # grant user write permission to do things like write account SPN
1696 sdutils = sd_utils.SDUtils(ldb)
1697 sdutils.dacl_add_ace(user_dn, "(A;;WP;;;PS)")
1700 duration = end - start
1701 LOGGER.info("%f\t0\tcreate\tuser\t%f\tTrue\t" % (end, duration))
1704 def create_group(ldb, instance_id, name):
1705 """Create a group via ldap."""
1707 ou = ou_name(ldb, instance_id)
1708 dn = "cn=%s,%s" % (name, ou)
1712 "objectclass": "group",
1713 "sAMAccountName": name,
1716 duration = end - start
1717 LOGGER.info("%f\t0\tcreate\tgroup\t%f\tTrue\t" % (end, duration))
1720 def user_name(instance_id, i):
1721 """Generate a user name based in the instance id"""
1722 return "STGU-%d-%d" % (instance_id, i)
1725 def search_objectclass(ldb, objectclass='user', attr='sAMAccountName'):
1726 """Seach objectclass, return attr in a set"""
1728 expression="(objectClass={})".format(objectclass),
1731 return {str(obj[attr]) for obj in objs}
1734 def generate_users(ldb, instance_id, number, password):
1735 """Add users to the server"""
1736 existing_objects = search_objectclass(ldb, objectclass='user')
1738 for i in range(number, 0, -1):
1739 name = user_name(instance_id, i)
1740 if name not in existing_objects:
1741 create_user_account(ldb, instance_id, name, password)
1747 def group_name(instance_id, i):
1748 """Generate a group name from instance id."""
1749 return "STGG-%d-%d" % (instance_id, i)
1752 def generate_groups(ldb, instance_id, number):
1753 """Create the required number of groups on the server."""
1754 existing_objects = search_objectclass(ldb, objectclass='group')
1756 for i in range(number, 0, -1):
1757 name = group_name(instance_id, i)
1758 if name not in existing_objects:
1759 create_group(ldb, instance_id, name)
1765 def clean_up_accounts(ldb, instance_id):
1766 """Remove the created accounts and groups from the server."""
1767 ou = ou_name(ldb, instance_id)
1769 ldb.delete(ou, ["tree_delete:1"])
1770 except LdbError as e:
1771 (status, _) = e.args
1772 # ignore does not exist
1777 def generate_users_and_groups(ldb, instance_id, password,
1778 number_of_users, number_of_groups,
1780 """Generate the required users and groups, allocating the users to
1785 create_ou(ldb, instance_id)
1787 print("Generating dummy user accounts", file=sys.stderr)
1788 users_added = generate_users(ldb, instance_id, number_of_users, password)
1790 if number_of_groups > 0:
1791 print("Generating dummy groups", file=sys.stderr)
1792 groups_added = generate_groups(ldb, instance_id, number_of_groups)
1794 if group_memberships > 0:
1795 print("Assigning users to groups", file=sys.stderr)
1796 assignments = assign_groups(number_of_groups,
1801 print("Adding users to groups", file=sys.stderr)
1802 add_users_to_groups(ldb, instance_id, assignments)
1804 if (groups_added > 0 and users_added == 0 and
1805 number_of_groups != groups_added):
1806 print("Warning: the added groups will contain no members",
1809 print(("Added %d users, %d groups and %d group memberships" %
1810 (users_added, groups_added, len(assignments))),
1814 def assign_groups(number_of_groups,
1819 """Allocate users to groups.
1821 The intention is to have a few users that belong to most groups, while
1822 the majority of users belong to a few groups.
1824 A few groups will contain most users, with the remaining only having a
1828 def generate_user_distribution(n):
1829 """Probability distribution of a user belonging to a group.
1832 for x in range(1, n + 1):
1837 def generate_group_distribution(n):
1838 """Probability distribution of a group containing a user."""
1840 for x in range(1, n + 1):
1846 if group_memberships <= 0:
1849 group_dist = generate_group_distribution(number_of_groups)
1850 user_dist = generate_user_distribution(number_of_users)
1852 # Calculate the number of group menberships required
1853 group_memberships = math.ceil(
1854 float(group_memberships) *
1855 (float(users_added) / float(number_of_users)))
1857 existing_users = number_of_users - users_added - 1
1858 existing_groups = number_of_groups - groups_added - 1
1859 while len(assignments) < group_memberships:
1860 user = random.randint(0, number_of_users - 1)
1861 group = random.randint(0, number_of_groups - 1)
1862 probability = group_dist[group] * user_dist[user]
1864 if ((random.random() < probability * 10000) and
1865 (group > existing_groups or user > existing_users)):
1866 # the + 1 converts the array index to the corresponding
1867 # group or user number
1868 assignments.add(((user + 1), (group + 1)))
1873 def add_users_to_groups(db, instance_id, assignments):
1874 """Add users to their assigned groups.
1876 Takes the list of (group,user) tuples generated by assign_groups and
1877 assign the users to their specified groups."""
1879 ou = ou_name(db, instance_id)
1882 return("cn=%s,%s" % (name, ou))
1884 for (user, group) in assignments:
1885 user_dn = build_dn(user_name(instance_id, user))
1886 group_dn = build_dn(group_name(instance_id, group))
1889 m.dn = ldb.Dn(db, group_dn)
1890 m["member"] = ldb.MessageElement(user_dn, ldb.FLAG_MOD_ADD, "member")
1894 duration = end - start
1895 print("%f\t0\tadd\tuser\t%f\tTrue\t" % (end, duration))
1898 def generate_stats(statsdir, timing_file):
1899 """Generate and print the summary stats for a run."""
1900 first = sys.float_info.max
1906 unique_converations = set()
1909 if timing_file is not None:
1910 tw = timing_file.write
1915 tw("time\tconv\tprotocol\ttype\tduration\tsuccessful\terror\n")
1917 for filename in os.listdir(statsdir):
1918 path = os.path.join(statsdir, filename)
1919 with open(path, 'r') as f:
1922 fields = line.rstrip('\n').split('\t')
1923 conversation = fields[1]
1924 protocol = fields[2]
1925 packet_type = fields[3]
1926 latency = float(fields[4])
1927 first = min(float(fields[0]) - latency, first)
1928 last = max(float(fields[0]), last)
1930 if protocol not in latencies:
1931 latencies[protocol] = {}
1932 if packet_type not in latencies[protocol]:
1933 latencies[protocol][packet_type] = []
1935 latencies[protocol][packet_type].append(latency)
1937 if protocol not in failures:
1938 failures[protocol] = {}
1939 if packet_type not in failures[protocol]:
1940 failures[protocol][packet_type] = 0
1942 if fields[5] == 'True':
1946 failures[protocol][packet_type] += 1
1948 if conversation not in unique_converations:
1949 unique_converations.add(conversation)
1953 except (ValueError, IndexError):
1954 # not a valid line print and ignore
1955 print(line, file=sys.stderr)
1957 duration = last - first
1961 success_rate = successful / duration
1965 failure_rate = failed / duration
1967 print("Total conversations: %10d" % conversations)
1968 print("Successful operations: %10d (%.3f per second)"
1969 % (successful, success_rate))
1970 print("Failed operations: %10d (%.3f per second)"
1971 % (failed, failure_rate))
1973 print("Protocol Op Code Description "
1974 " Count Failed Mean Median "
1977 protocols = sorted(latencies.keys())
1978 for protocol in protocols:
1979 packet_types = sorted(latencies[protocol], key=opcode_key)
1980 for packet_type in packet_types:
1981 values = latencies[protocol][packet_type]
1982 values = sorted(values)
1984 failed = failures[protocol][packet_type]
1985 mean = sum(values) / count
1986 median = calc_percentile(values, 0.50)
1987 percentile = calc_percentile(values, 0.95)
1988 rng = values[-1] - values[0]
1990 desc = OP_DESCRIPTIONS.get((protocol, packet_type), '')
1991 if sys.stdout.isatty:
1992 print("%-12s %4s %-35s %12d %12d %12.6f "
1993 "%12.6f %12.6f %12.6f %12.6f"
2005 print("%s\t%s\t%s\t%d\t%d\t%f\t%f\t%f\t%f\t%f"
2019 """Sort key for the operation code to ensure that it sorts numerically"""
2021 return "%03d" % int(v)
2026 def calc_percentile(values, percentile):
2027 """Calculate the specified percentile from the list of values.
2029 Assumes the list is sorted in ascending order.
2034 k = (len(values) - 1) * percentile
2038 return values[int(k)]
2039 d0 = values[int(f)] * (c - k)
2040 d1 = values[int(c)] * (k - f)
2044 def mk_masked_dir(*path):
2045 """In a testenv we end up with 0777 diectories that look an alarming
2046 green colour with ls. Use umask to avoid that."""
2047 d = os.path.join(*path)
2048 mask = os.umask(0o077)