server.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. #!/usr/bin/env python2
  2. import re
  3. import os
  4. import time
  5. import atexit
  6. import signal
  7. import ipaddress
  8. from random import randint
  9. from threading import Thread
  10. from threading import Lock
  11. import redis
  12. import json
  13. import iptc
  14. import dns.resolver
  15. import dns.exception
  16. while True:
  17. try:
  18. r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
  19. r.ping()
  20. except Exception as ex:
  21. print '%s - trying again in 3 seconds' % (ex)
  22. time.sleep(3)
  23. else:
  24. break
  25. pubsub = r.pubsub()
  26. resolver = dns.resolver.Resolver()
  27. RULES = {}
  28. RULES[1] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed'
  29. RULES[2] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
  30. RULES[3] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  31. RULES[4] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
  32. RULES[5] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
  33. RULES[6] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
  34. #RULES[7] = '-login: Aborted login \(no auth .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
  35. bans = {}
  36. log = {}
  37. quit_now = False
  38. lock = Lock()
  39. def is_ip_network(address):
  40. try:
  41. ipaddress.ip_network(address.decode('ascii'), False)
  42. except ValueError:
  43. return False
  44. return True
  45. def refreshF2boptions():
  46. global f2boptions
  47. global quit_now
  48. if not r.get('F2B_OPTIONS'):
  49. f2boptions = {}
  50. f2boptions['ban_time'] = int
  51. f2boptions['max_attempts'] = int
  52. f2boptions['retry_window'] = int
  53. f2boptions['netban_ipv4'] = int
  54. f2boptions['netban_ipv6'] = int
  55. f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
  56. f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
  57. f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
  58. f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 24
  59. f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 64
  60. r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
  61. else:
  62. try:
  63. f2boptions = {}
  64. f2boptions = json.loads(r.get('F2B_OPTIONS'))
  65. except ValueError, e:
  66. print 'Error loading F2B options: F2B_OPTIONS is not json'
  67. quit_now = True
  68. if r.exists('F2B_LOG'):
  69. r.rename('F2B_LOG', 'NETFILTER_LOG')
  70. def mailcowChainOrder():
  71. global lock
  72. global quit_now
  73. while not quit_now:
  74. time.sleep(10)
  75. with lock:
  76. filter4_table = iptc.Table(iptc.Table.FILTER)
  77. filter6_table = iptc.Table6(iptc.Table6.FILTER)
  78. filter4_table.refresh()
  79. filter6_table.refresh()
  80. for f in [filter4_table, filter6_table]:
  81. forward_chain = iptc.Chain(f, 'FORWARD')
  82. input_chain = iptc.Chain(f, 'INPUT')
  83. for chain in [forward_chain, input_chain]:
  84. target_found = False
  85. for position, item in enumerate(chain.rules):
  86. if item.target.name == 'MAILCOW':
  87. target_found = True
  88. if position != 0:
  89. log['time'] = int(round(time.time()))
  90. log['priority'] = 'crit'
  91. log['message'] = 'Error in ' + chain.name + ' chain order, restarting container'
  92. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  93. print log['message']
  94. quit_now = True
  95. if not target_found:
  96. log['time'] = int(round(time.time()))
  97. log['priority'] = 'crit'
  98. log['message'] = 'Error in ' + chain.name + ' chain: MAILCOW target not found, restarting container'
  99. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  100. print log['message']
  101. quit_now = True
  102. def ban(address):
  103. global lock
  104. refreshF2boptions()
  105. BAN_TIME = int(f2boptions['ban_time'])
  106. MAX_ATTEMPTS = int(f2boptions['max_attempts'])
  107. RETRY_WINDOW = int(f2boptions['retry_window'])
  108. NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
  109. NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
  110. WHITELIST = r.hgetall('F2B_WHITELIST')
  111. ip = ipaddress.ip_address(address.decode('ascii'))
  112. if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
  113. ip = ip.ipv4_mapped
  114. address = str(ip)
  115. if ip.is_private or ip.is_loopback:
  116. return
  117. self_network = ipaddress.ip_network(address.decode('ascii'))
  118. if WHITELIST:
  119. wl_hostnames=[]
  120. wl_networks=[]
  121. for wl_key in WHITELIST:
  122. if is_ip_network(wl_key):
  123. wl_networks.append(wl_key)
  124. else:
  125. wl_hostnames.append(wl_key)
  126. for w1_hostname in wl_hostnames:
  127. hostname_ips = []
  128. for rdtype in ['A', 'AAAA']:
  129. try:
  130. answer = resolver.query(qname=w1_hostname, rdtype=rdtype, lifetime=1)
  131. except dns.exception.Timeout as timout:
  132. log['time'] = int(round(time.time()))
  133. log['priority'] = 'info'
  134. log['message'] = 'Hostname %s timedout on resolve' % (w1_hostname)
  135. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  136. print 'Hostname %s timedout on resolve' % (w1_hostname)
  137. break
  138. except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
  139. continue
  140. except dns.exception.DNSException as dnsexception:
  141. log['time'] = int(round(time.time()))
  142. log['priority'] = 'info'
  143. log['message'] = '%s' % (dnsexception)
  144. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  145. print '%s' % (dnsexception)
  146. continue
  147. for rdata in answer:
  148. hostname_ips.append(rdata.to_text())
  149. wl_networks.extend(hostname_ips)
  150. log['time'] = int(round(time.time()))
  151. log['priority'] = 'info'
  152. log['message'] = 'Hostname %s is resolved to %s' % (w1_hostname, hostname_ips)
  153. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  154. print 'Hostname %s is resolved to %s' % (w1_hostname, hostname_ips)
  155. for wl_key in wl_networks:
  156. wl_net = ipaddress.ip_network(wl_key.decode('ascii'), False)
  157. if wl_net.overlaps(self_network):
  158. log['time'] = int(round(time.time()))
  159. log['priority'] = 'info'
  160. log['message'] = 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
  161. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  162. print 'Address %s is whitelisted by rule %s' % (self_network, wl_net)
  163. return
  164. net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)).decode('ascii'), strict=False)
  165. net = str(net)
  166. if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
  167. bans[net] = { 'attempts': 0 }
  168. active_window = RETRY_WINDOW
  169. else:
  170. active_window = time.time() - bans[net]['last_attempt']
  171. bans[net]['attempts'] += 1
  172. bans[net]['last_attempt'] = time.time()
  173. active_window = time.time() - bans[net]['last_attempt']
  174. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  175. log['time'] = int(round(time.time()))
  176. log['priority'] = 'crit'
  177. log['message'] = 'Banning %s' % net
  178. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  179. print 'Banning %s for %d minutes' % (net, BAN_TIME / 60)
  180. if type(ip) is ipaddress.IPv4Address:
  181. with lock:
  182. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  183. rule = iptc.Rule()
  184. rule.src = net
  185. target = iptc.Target(rule, "REJECT")
  186. rule.target = target
  187. if rule not in chain.rules:
  188. chain.insert_rule(rule)
  189. else:
  190. with lock:
  191. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  192. rule = iptc.Rule6()
  193. rule.src = net
  194. target = iptc.Target(rule, "REJECT")
  195. rule.target = target
  196. if rule not in chain.rules:
  197. chain.insert_rule(rule)
  198. r.hset('F2B_ACTIVE_BANS', '%s' % net, log['time'] + BAN_TIME)
  199. else:
  200. log['time'] = int(round(time.time()))
  201. log['priority'] = 'warn'
  202. log['message'] = '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  203. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  204. print '%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)
  205. def unban(net):
  206. global lock
  207. log['time'] = int(round(time.time()))
  208. log['priority'] = 'info'
  209. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  210. if not net in bans:
  211. log['message'] = '%s is not banned, skipping unban and deleting from queue (if any)' % net
  212. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  213. print '%s is not banned, skipping unban and deleting from queue (if any)' % net
  214. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  215. return
  216. log['message'] = 'Unbanning %s' % net
  217. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  218. print 'Unbanning %s' % net
  219. if type(ipaddress.ip_network(net.decode('ascii'))) is ipaddress.IPv4Network:
  220. with lock:
  221. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  222. rule = iptc.Rule()
  223. rule.src = net
  224. target = iptc.Target(rule, "REJECT")
  225. rule.target = target
  226. if rule in chain.rules:
  227. chain.delete_rule(rule)
  228. else:
  229. with lock:
  230. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  231. rule = iptc.Rule6()
  232. rule.src = net
  233. target = iptc.Target(rule, "REJECT")
  234. rule.target = target
  235. if rule in chain.rules:
  236. chain.delete_rule(rule)
  237. r.hdel('F2B_ACTIVE_BANS', '%s' % net)
  238. r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
  239. if net in bans:
  240. del bans[net]
  241. def quit(signum, frame):
  242. global quit_now
  243. quit_now = True
  244. def clear():
  245. global lock
  246. log['time'] = int(round(time.time()))
  247. log['priority'] = 'info'
  248. log['message'] = 'Clearing all bans'
  249. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  250. print 'Clearing all bans'
  251. for net in bans.copy():
  252. unban(net)
  253. with lock:
  254. filter4_table = iptc.Table(iptc.Table.FILTER)
  255. filter6_table = iptc.Table6(iptc.Table6.FILTER)
  256. for filter_table in [filter4_table, filter6_table]:
  257. filter_table.autocommit = False
  258. forward_chain = iptc.Chain(filter_table, "FORWARD")
  259. input_chain = iptc.Chain(filter_table, "INPUT")
  260. mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
  261. if mailcow_chain in filter_table.chains:
  262. for rule in mailcow_chain.rules:
  263. mailcow_chain.delete_rule(rule)
  264. for rule in forward_chain.rules:
  265. if rule.target.name == 'MAILCOW':
  266. forward_chain.delete_rule(rule)
  267. for rule in input_chain.rules:
  268. if rule.target.name == 'MAILCOW':
  269. input_chain.delete_rule(rule)
  270. filter_table.delete_chain("MAILCOW")
  271. filter_table.commit()
  272. filter_table.refresh()
  273. filter_table.autocommit = True
  274. r.delete('F2B_ACTIVE_BANS')
  275. r.delete('F2B_PERM_BANS')
  276. pubsub.unsubscribe()
  277. def watch():
  278. log['time'] = int(round(time.time()))
  279. log['priority'] = 'info'
  280. log['message'] = 'Watching Redis channel F2B_CHANNEL'
  281. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  282. pubsub.subscribe('F2B_CHANNEL')
  283. print 'Subscribing to Redis channel F2B_CHANNEL'
  284. while not quit_now:
  285. for item in pubsub.listen():
  286. for rule_id, rule_regex in RULES.iteritems():
  287. if item['data'] and item['type'] == 'message':
  288. result = re.search(rule_regex, item['data'])
  289. if result:
  290. addr = result.group(1)
  291. ip = ipaddress.ip_address(addr.decode('ascii'))
  292. if ip.is_private or ip.is_loopback:
  293. continue
  294. print '%s matched rule id %d' % (addr, rule_id)
  295. log['time'] = int(round(time.time()))
  296. log['priority'] = 'warn'
  297. log['message'] = '%s matched rule id %d' % (addr, rule_id)
  298. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  299. ban(addr)
  300. def snat4(snat_target):
  301. global lock
  302. global quit_now
  303. def get_snat4_rule():
  304. rule = iptc.Rule()
  305. rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
  306. rule.dst = '!' + rule.src
  307. target = rule.create_target("SNAT")
  308. target.to_source = snat_target
  309. return rule
  310. while not quit_now:
  311. time.sleep(10)
  312. with lock:
  313. try:
  314. table = iptc.Table('nat')
  315. table.refresh()
  316. chain = iptc.Chain(table, 'POSTROUTING')
  317. table.autocommit = False
  318. if get_snat4_rule() not in chain.rules:
  319. log['time'] = int(round(time.time()))
  320. log['priority'] = 'info'
  321. log['message'] = 'Added POSTROUTING rule for source network ' + get_snat4_rule().src + ' to SNAT target ' + snat_target
  322. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  323. print log['message']
  324. chain.insert_rule(get_snat4_rule())
  325. table.commit()
  326. else:
  327. for position, item in enumerate(chain.rules):
  328. if item == get_snat4_rule():
  329. if position != 0:
  330. chain.delete_rule(get_snat4_rule())
  331. table.commit()
  332. table.autocommit = True
  333. except:
  334. print 'Error running SNAT4, retrying...'
  335. def snat6(snat_target):
  336. global lock
  337. global quit_now
  338. def get_snat6_rule():
  339. rule = iptc.Rule6()
  340. rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
  341. rule.dst = '!' + rule.src
  342. target = rule.create_target("SNAT")
  343. target.to_source = snat_target
  344. return rule
  345. while not quit_now:
  346. time.sleep(10)
  347. with lock:
  348. try:
  349. table = iptc.Table6('nat')
  350. table.refresh()
  351. chain = iptc.Chain(table, 'POSTROUTING')
  352. table.autocommit = False
  353. if get_snat6_rule() not in chain.rules:
  354. log['time'] = int(round(time.time()))
  355. log['priority'] = 'info'
  356. log['message'] = 'Added POSTROUTING rule for source network ' + get_snat6_rule().src + ' to SNAT target ' + snat_target
  357. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  358. print log['message']
  359. chain.insert_rule(get_snat6_rule())
  360. table.commit()
  361. else:
  362. for position, item in enumerate(chain.rules):
  363. if item == get_snat6_rule():
  364. if position != 0:
  365. chain.delete_rule(get_snat6_rule())
  366. table.commit()
  367. table.autocommit = True
  368. except:
  369. print 'Error running SNAT6, retrying...'
  370. def autopurge():
  371. while not quit_now:
  372. time.sleep(10)
  373. refreshF2boptions()
  374. BAN_TIME = f2boptions['ban_time']
  375. MAX_ATTEMPTS = f2boptions['max_attempts']
  376. QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
  377. if QUEUE_UNBAN:
  378. for net in QUEUE_UNBAN:
  379. unban(str(net))
  380. for net in bans.copy():
  381. if bans[net]['attempts'] >= MAX_ATTEMPTS:
  382. if time.time() - bans[net]['last_attempt'] > BAN_TIME:
  383. unban(net)
  384. def initChain():
  385. # Is called before threads start, no locking
  386. print "Initializing mailcow netfilter chain"
  387. # IPv4
  388. if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
  389. iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
  390. for c in ['FORWARD', 'INPUT']:
  391. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
  392. rule = iptc.Rule()
  393. rule.src = '0.0.0.0/0'
  394. rule.dst = '0.0.0.0/0'
  395. target = iptc.Target(rule, "MAILCOW")
  396. rule.target = target
  397. if rule not in chain.rules:
  398. chain.insert_rule(rule)
  399. # IPv6
  400. if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
  401. iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
  402. for c in ['FORWARD', 'INPUT']:
  403. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
  404. rule = iptc.Rule6()
  405. rule.src = '::/0'
  406. rule.dst = '::/0'
  407. target = iptc.Target(rule, "MAILCOW")
  408. rule.target = target
  409. if rule not in chain.rules:
  410. chain.insert_rule(rule)
  411. # Apply blacklist
  412. BLACKLIST = r.hgetall('F2B_BLACKLIST')
  413. if BLACKLIST:
  414. for bl_key in BLACKLIST:
  415. if type(ipaddress.ip_network(bl_key.decode('ascii'), strict=False)) is ipaddress.IPv4Network:
  416. chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
  417. rule = iptc.Rule()
  418. rule.src = bl_key
  419. target = iptc.Target(rule, "REJECT")
  420. rule.target = target
  421. if rule not in chain.rules:
  422. log['time'] = int(round(time.time()))
  423. log['priority'] = 'crit'
  424. log['message'] = 'Blacklisting host/network %s' % bl_key
  425. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  426. print log['message']
  427. chain.insert_rule(rule)
  428. r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
  429. else:
  430. chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
  431. rule = iptc.Rule6()
  432. rule.src = bl_key
  433. target = iptc.Target(rule, "REJECT")
  434. rule.target = target
  435. if rule not in chain.rules:
  436. log['time'] = int(round(time.time()))
  437. log['priority'] = 'crit'
  438. log['message'] = 'Blacklisting host/network %s' % bl_key
  439. r.lpush('NETFILTER_LOG', json.dumps(log, ensure_ascii=False))
  440. print log['message']
  441. chain.insert_rule(rule)
  442. r.hset('F2B_PERM_BANS', '%s' % bl_key, int(round(time.time())))
  443. if __name__ == '__main__':
  444. # In case a previous session was killed without cleanup
  445. clear()
  446. # Reinit MAILCOW chain
  447. initChain()
  448. watch_thread = Thread(target=watch)
  449. watch_thread.daemon = True
  450. watch_thread.start()
  451. if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') is not 'n':
  452. try:
  453. snat_ip = os.getenv('SNAT_TO_SOURCE').decode('ascii')
  454. snat_ipo = ipaddress.ip_address(snat_ip)
  455. if type(snat_ipo) is ipaddress.IPv4Address:
  456. snat4_thread = Thread(target=snat4,args=(snat_ip,))
  457. snat4_thread.daemon = True
  458. snat4_thread.start()
  459. except ValueError:
  460. print os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address'
  461. if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') is not 'n':
  462. try:
  463. snat_ip = os.getenv('SNAT6_TO_SOURCE').decode('ascii')
  464. snat_ipo = ipaddress.ip_address(snat_ip)
  465. if type(snat_ipo) is ipaddress.IPv6Address:
  466. snat6_thread = Thread(target=snat6,args=(snat_ip,))
  467. snat6_thread.daemon = True
  468. snat6_thread.start()
  469. except ValueError:
  470. print os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address'
  471. autopurge_thread = Thread(target=autopurge)
  472. autopurge_thread.daemon = True
  473. autopurge_thread.start()
  474. mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
  475. mailcowchainwatch_thread.daemon = True
  476. mailcowchainwatch_thread.start()
  477. signal.signal(signal.SIGTERM, quit)
  478. atexit.register(clear)
  479. while not quit_now:
  480. time.sleep(0.5)