PageRenderTime 52ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/client.py

https://bitbucket.org/uva-sne/vnet-ui-cli
Python | 426 lines | 362 code | 53 blank | 11 comment | 109 complexity | 952ab6e33d70ae1cfeeb3fb7a3ca91bd MD5 | raw file
  1. import websockets
  2. import logging
  3. import json
  4. import asyncio
  5. logging.SPAM = 5
  6. def domainFromName(name):
  7. for part in name.split('.'):
  8. if part.startswith('as'):
  9. return part[2:]
  10. class SarnetUIConnection():
  11. def __init__(self, uri, ssl):
  12. self.ws = None
  13. self.uri = uri
  14. self.ctx = ssl
  15. self.identifier = {'version': 'sc17-0.1',
  16. 'agent': 'python3/websockets',
  17. 'screen': '0x0@1',
  18. 'url': 'cli'}
  19. async def __aenter__(self):
  20. self._conn = websockets.connect(self.uri, ssl=self.ctx)
  21. self.ws = await self._conn.__aenter__()
  22. await self.oldsend('identify', data=self.identifier)
  23. return self
  24. async def __aexit__(self, *args, **kwargs):
  25. await self._conn.__aexit__(*args, **kwargs)
  26. async def recieve(self):
  27. try:
  28. msg = await asyncio.wait_for(self.ws.recv(), timeout=20)
  29. except asyncio.TimeoutError:
  30. # No data in 20 seconds, check the connection.
  31. try:
  32. pong_waiter = await self.ws.ping()
  33. await asyncio.wait_for(pong_waiter, timeout=10)
  34. except asyncio.TimeoutError:
  35. # No response to ping in 10 seconds, disconnect.
  36. raise asyncio.TimeoutError
  37. else:
  38. logging.log(logging.SPAM, 'recv: %s', msg)
  39. parsed = json.loads(msg)
  40. return parsed
  41. async def _send(self, msg):
  42. logging.debug('sent: %s', json.dumps(msg))
  43. await self.ws.send(json.dumps(msg))
  44. async def recv_handler(self):
  45. while True:
  46. parsed = await self.recieve()
  47. self.handle_msg(parsed)
  48. async def newsend(self, kind, **kwargs):
  49. msg = {'@': kind}
  50. if msg:
  51. msg.update(kwargs)
  52. await self._send(msg)
  53. async def oldsend(self, kind, data=None, **kwargs):
  54. msg = {'kind': kind}
  55. if data is not None or kwargs:
  56. if data:
  57. data.update(kwargs)
  58. else:
  59. data = kwargs
  60. msg['data'] = data
  61. await self._send(msg)
  62. def handle_msg(self, msg):
  63. if 'm' in msg and 'cmd/host-netctl/containers/va' in msg['m']:
  64. logging.info('got: %s', msg)
  65. class SarnetUIClient(SarnetUIConnection):
  66. def __init__(self, *args, **kwargs):
  67. super().__init__(*args, **kwargs)
  68. self.topology = None
  69. self.metadata = {}
  70. self.nodes = []
  71. self.possible_attackers = []
  72. self.possible_victims = []
  73. self.scenario = None
  74. self.msg_callback = {}
  75. self.learning = set()
  76. self.thresholds = {}
  77. def handle_msg(self, msg):
  78. if msg['@'] == 't':
  79. self.setTopology(msg['t'])
  80. elif msg['@'] == 'm': # metadata updates
  81. self.setNodeMetadata(msg['i'], msg['m'])
  82. elif msg['@'] == 'M': # node metadata
  83. for node_name in msg['M'].keys():
  84. self.nodes.append(node_name)
  85. self.setNodeMetadata(node_name, msg['M'][node_name], True)
  86. elif msg['@'] == 'f':
  87. # flows
  88. pass
  89. elif msg['@'] == 'B':
  90. if msg['B']['kind'] == 'reload-topo':
  91. logging.warning('reload-topo requested')
  92. # reload topo
  93. pass
  94. else:
  95. logging.debug('client: got unknown msg: %', msg)
  96. async def set_collab(self, level):
  97. if level in ['local', 'upstream', 'alliance'] or level.isdigit():
  98. if level == '0':
  99. level = 'local'
  100. await self._send({"@": "pub",
  101. "k": "cmd/sarnet/collaboration",
  102. "v": level,
  103. "r": True})
  104. logging.debug('client: set level to %s', level)
  105. return level
  106. return None
  107. async def set_defense(self, level):
  108. if level in ['filter', 'rateup', 'honeypot']:
  109. await self._send({"@": "pub",
  110. "k": "cmd/sarnet/defense",
  111. "v": level,
  112. "r": True})
  113. logging.debug('client: set defense to %s', level)
  114. return level
  115. return None
  116. def setTopology(self, topology):
  117. self.topology = topology
  118. self.topology['flow_owner'] = {}
  119. self.topology['stat_owner'] = {}
  120. flowMacs = {}
  121. for k, v in topology['nodes'].items():
  122. if '/service/' in k:
  123. self.possible_victims.append(k)
  124. if '/client/' in k:
  125. self.possible_attackers.append(k)
  126. a = self.metadata.setdefault(v['name'], {})
  127. a['iflink'] = {}
  128. a['ifmac'] = {}
  129. for iface in v['ifaces']:
  130. if 'links' not in iface or len(iface['links']) == 0:
  131. continue
  132. linkName = iface['links'][0]
  133. try:
  134. link = topology['links'][linkName]
  135. except KeyError:
  136. link = None
  137. if not link:
  138. continue
  139. if 'ifname' in iface:
  140. a.iflink[linkName] = iface['ifname']
  141. if iface['mac']:
  142. mac = iface['mac'].lower()
  143. a['ifmac'][mac] = linkName
  144. flowMacs[mac.replace(':', '')] = {
  145. 'left': link['source'],
  146. 'right': link['target'],
  147. 'owner': v['name'],
  148. 'link': linkName}
  149. def setNodeNetstats(self, name, kv):
  150. # don't seem to need it
  151. pass
  152. def trackValue(self, kv, k, v):
  153. if v:
  154. kv[k] = v
  155. else:
  156. kv.pop(k, None)
  157. def setNodeMetadata(self, name, kv, reset=False):
  158. meta = {}
  159. if not kv:
  160. self.metadata.pop(name, None)
  161. meta = {}
  162. kv = {}
  163. reset = True
  164. else:
  165. meta = self.metadata.setdefault(name, {})
  166. if reset:
  167. meta['status'] = {}
  168. meta['linkStatus'] = {}
  169. # 'undefined' may be Null
  170. if 'status' not in meta.keys() or meta['status'] is None:
  171. meta['status'] = {}
  172. if 'linkStatus' not in meta.keys() or meta['linkStatus'] is None:
  173. meta['linkStatus'] = {}
  174. if 'vm/ns' in kv.keys():
  175. self.setNodeNetstats(name, kv['vm/ns'])
  176. for k, v in kv.items():
  177. if k.startswith('if/'):
  178. iface = json.loads(v)
  179. # idgi
  180. if iface['mac'] and iface['iface']:
  181. mac = iface['mac'].lower()
  182. meta.setdefault('iflink', {})[iface['iface']] = mac
  183. if 'ifmac' in meta:
  184. meta.setdefault('iflink', {})[meta['ifmac'][mac]] \
  185. = iface['iface']
  186. elif k.startswith('sarnet/a/'):
  187. self.trackValue(meta['status'], k, v or False)
  188. elif k.startswith('sarnet/o/'):
  189. self.trackValue(meta['status'], k, v != '{"state": true}')
  190. elif k == 'sarnet/log':
  191. # node log
  192. pass
  193. elif k == 'sarnet/learn_mode':
  194. self.trackValue(meta['status'], k, v is True)
  195. pass
  196. elif k == 'cmd/host-netctl/iface-filter':
  197. pass
  198. elif k == 'cmd/host-netctl/sdn-redirect':
  199. pass
  200. elif k == 'cmd/host-netctl/nfv':
  201. self.trackValue(meta['status'], k, v != '[]')
  202. elif k == 'vm/containers':
  203. atk = False
  204. cname = v
  205. for cname in json.loads(v):
  206. if cname.startswith('va-'):
  207. atk = True
  208. self.trackValue(meta['status'], '$attacker', atk)
  209. elif k == 'vm/egress-policer':
  210. meta['linkStatus']['policer'] = json.loads(v)
  211. pass
  212. for pattern, callback in self.msg_callback.items():
  213. if callback and k.startswith(pattern):
  214. callback(name, k, v)
  215. def set_callback(self, pattern, func):
  216. self.msg_callback[pattern] = func
  217. async def stop(self):
  218. self.scenario = None
  219. await self._send({'@': 'attack', 'attack': 'stop'})
  220. async def set_behaviour(self, data):
  221. logging.info('client: broadcasting domain behaviour parameters')
  222. await self._send({'@': 'behaviour', 'data': data})
  223. def N(self, name):
  224. def url_to_ident(ident):
  225. if ident.startswith('http://geni-orca.renci.org/owl/'):
  226. return ident[31:]
  227. return ident
  228. n = self.topology['nodes'][name]
  229. if n and 'unit_url' in n.keys():
  230. return url_to_ident(n['unit_url'])
  231. return name
  232. async def attack(self, att_type, attackers, victim, rate=.8):
  233. def ddos_rate(attackers, rate):
  234. max_link_capacity = 100
  235. max_rate = max_link_capacity * len(attackers)
  236. if rate == 'high':
  237. rate = .9
  238. elif rate == 'med':
  239. rate = .6
  240. elif rate == 'low':
  241. rate = .3
  242. return max_rate * rate
  243. def pwd_rate(rate):
  244. if rate == 'high':
  245. rate = .9
  246. elif rate == 'med':
  247. rate = .6
  248. elif rate == 'low':
  249. rate = .3
  250. return int(10 * rate)
  251. async def attack_victim(att_type, attackers, victim, rate=.8):
  252. victimDomain = domainFromName(victim)
  253. nrate = ddos_rate(attackers, rate)
  254. nworkers = pwd_rate(rate)
  255. logging.info('ddos rate = %s, pwd workers = %s, victim=%s', str(nrate),
  256. str(int(nworkers)), victim)
  257. d = {'@': 'attack', 'attack': 'cs', 'cs': {}}
  258. for a in attackers:
  259. localDomain = domainFromName(a)
  260. env = {}
  261. docker_img = None
  262. if att_type == 'pw':
  263. docker_img = 'vnet-client'
  264. env['CONTAINER_ARGS'] = '-mode,pw,-workers,' + str(int(nworkers)) + ',172.30.' + victimDomain + '.64'
  265. elif att_type == 'reflect':
  266. docker_img = 'vnet-ddos-udp'
  267. logging.debug('client: reflect select: %s %s',
  268. localDomain, int(localDomain) < 13)
  269. env['SRC'] = '172.30.' + victimDomain + '.250'
  270. # TODO: need a reflector select mechanism
  271. env['DST'] = '172.30.' + ('15' if int(localDomain) < 13
  272. else '11') + '.63'
  273. env['DST'] = '172.30.' + '11' + '.63'
  274. env['RATE'] = str((nrate / (3 * len(attackers)) or 0))
  275. env['SIZE'] = '500'
  276. else:
  277. docker_img = 'vnet-ddos-udp'
  278. env['SRC'] = '172.30.' + localDomain + '.250'
  279. env['DST'] = '172.30.' + victimDomain + '.250'
  280. env['RATE'] = str(min(95, nrate / len(attackers) or 0))
  281. env['SIZE'] = '1460'
  282. d['cs'][self.N(a)] = [{'name': 'va-attack',
  283. 'img': docker_img,
  284. 'env': env}]
  285. for a in self.possible_attackers:
  286. if a not in attackers:
  287. d['cs'][self.N(a)] = []
  288. await self._send(d)
  289. if isinstance(victim, list):
  290. n_victims = len(victim)
  291. n_attackers = len(attackers)
  292. n_att_vic = int(n_attackers / n_victims)
  293. if n_attackers < n_victims:
  294. raise ValueError("num_attackers less than num_victims")
  295. # can improve it with someting modulo etc.
  296. # though better to error because it's probably
  297. # unintentional if this occurs
  298. for i, v in enumerate(victim):
  299. idx = n_att_vic * i
  300. att = attackers[idx:idx + n_att_vic]
  301. await attack_victim(att_type, att, v, rate)
  302. else:
  303. await attack_victim(att_type, attackers, victim, rate)
  304. async def start(self, scenario):
  305. if self.scenario:
  306. raise RuntimeError('already running scenario')
  307. self.scenario = scenario
  308. if scenario.attack not in ['pw', 'reflect', 'ddos']:
  309. return
  310. if scenario.level:
  311. await self.set_collab(scenario.level)
  312. if scenario.defense:
  313. await self.set_defense(scenario.defense)
  314. if scenario.behaviour:
  315. await self.set_behaviour(scenario.behaviour)
  316. if scenario.rate:
  317. rate = scenario.rate
  318. else:
  319. rate = .3
  320. await self.attack(scenario.attack, scenario.attackers,
  321. scenario.victims, rate)
  322. async def clean(self, ignore=[]):
  323. logging.info('cleanup: start')
  324. clean = False
  325. while not clean:
  326. need_cleaning = []
  327. await asyncio.sleep(1)
  328. logging.debug('cleanup: cleaning...')
  329. for name in self.topology['nodes']:
  330. m = self.metadata[name]
  331. if not m:
  332. continue
  333. ignore_obs = any([i for i in ignore if i in name])
  334. if 'status' not in m.keys():
  335. continue
  336. for k in m['status'].keys():
  337. if ignore_obs and k.startswith('sarnet/o'):
  338. logging.debug('cleanup: ignoring %s', name)
  339. continue
  340. need_cleaning.append('{}/{}'.format(name, k))
  341. if len(need_cleaning) == 0:
  342. logging.info('cleanup: done')
  343. clean = True
  344. else:
  345. logging.debug('cleanup: need to clean up: %s', need_cleaning)
  346. return clean
  347. async def learn(self):
  348. """ Relearns sarnet's baselines (exposes thresholds) """
  349. await self._send({"@": "pub",
  350. "k": "cmd/sarnet/learn",
  351. "v": True,
  352. "r": True})
  353. logging.info('learning: start')
  354. self.set_callback('sarnet/thresh', self.handle_thresh)
  355. self.set_callback('sarnet/learn_mode', self.handle_learn)
  356. await asyncio.sleep(.5)
  357. while len(self.learning) > 0:
  358. await asyncio.sleep(1)
  359. logging.info('learning: ended')
  360. def handle_thresh(self, name, key, value):
  361. key = key.split('/')[2]
  362. self.thresholds.setdefault(name, {})[key.split('@')[0]] = float(value)
  363. def handle_learn(self, name, key, value):
  364. if value == 'true':
  365. self.learning.add(name)
  366. elif value == 'false':
  367. if name in self.learning:
  368. self.learning.remove(name)
  369. else:
  370. RuntimeError('expected true or false')