-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinput_processing.py
287 lines (247 loc) · 9.71 KB
/
input_processing.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
from datetime import datetime, timedelta
from select import select
from socket import AF_INET, socket
from typing import List, Tuple
from output_processing import deletion_process, pool
from packet import ResponseEntry, ResponsePacket, read_packet, validate_packet
from routeentry import RouteEntry
from routerbase import logger
from routingtable import RoutingTable
from validate_data import INFINITY, MAX_ID, MAX_METRIC, MIN_ID, MIN_METRIC
def validate_entry(table: RoutingTable, packet_entry: ResponseEntry) -> bool:
"""Validates an individual router entry."""
if packet_entry.router_id == table.router_id:
return False
# Checks the entry's AFI value
if packet_entry.afi != AF_INET:
logger(
f"The value {packet_entry.afi} for AFI does not match the "
f"expected value of AF_INET = {AF_INET}.",
is_debug=True,
)
return False
# Checks for the router_id
if packet_entry.router_id < MIN_ID or packet_entry.router_id > MAX_ID:
logger(
f"The entry's router id of {packet_entry.router_id} should be an "
f"integer between {MIN_ID} and {MAX_ID}, inclusive.",
is_debug=True,
)
return False
# Checks that the entry's metric is between the expected minimum and
# maximum metric.
if packet_entry.metric < MIN_METRIC or packet_entry.metric > MAX_METRIC:
logger(
f"The entry's metric of {packet_entry.metric} was not between the "
f"expected range of {MIN_METRIC} and {MAX_METRIC}, inclusive.",
is_debug=True,
)
return False
# If all the checks pass, then return `True`
return True
def add_route(
table: RoutingTable,
packet_entry: ResponseEntry,
new_metric: int,
next_hop: int,
sock: socket,
):
"""Adds a newly learned route to the routing table."""
if new_metric == INFINITY:
# Don't add routes with costs of infinity to the database - there's
# no point adding routes that you cannot reach.
return
logger(f"Adding route with cost of {new_metric}", is_debug=True)
actual_port = table.config_table[next_hop].port
entry = RouteEntry(actual_port, new_metric, table.timeout_delta, next_hop)
entry.gc_time = None
entry.flag = True
table.add_route(packet_entry.router_id, entry)
# NOTE: As per the assignment spec, "implement triggered updates when
# routes become invalid (i.e. when a router sets the routes metric to
# 16 <INFINITY> for whatever reason, compare end of page 24 and
# beginning of 25 in [1]), not for other metric updates or new routes".
# Thus, the following line is commented out, and the success lines added.
# send_responses(table, sock)
def adopt_route(
table: RoutingTable,
table_entry: RouteEntry,
new_metric: int,
next_hop: int,
sock: socket,
router_id: int,
):
"""
Adopts the newly received route, and updates the existing routing table
entry.
"""
logger(
f"Adopting route with metric of {new_metric}, router_id {router_id}",
is_debug=True,
)
table_entry.metric = new_metric
table_entry.next_hop = next_hop
table_entry.flag = True
table_entry.next_hop = next_hop
table_entry.triggered_update_time = None
# NOTE: As per the assignment spec, "implement triggered updates when
# routes become invalid (i.e. when a router sets the routes metric to
# 16 <INFINITY> for whatever reason, compare end of page 24 and
# beginning of 25 in [1]), not for other metric updates or new routes".
# Thus, the following line is commented out.
# send_responses(table, sock)
if new_metric == INFINITY:
# The following will eventually cause a triggered update.
logger("About to go into deletion process", is_debug=True)
pool.submit(deletion_process, table, sock, router_id)
else:
table_entry.update_timeout_time(table.timeout_delta)
def update_table(
table: RoutingTable,
packet_entry: ResponseEntry,
new_metric: int,
next_hop: int,
sock: socket,
):
"""
Goes through the process of updating the routing table with the new route,
if applicable.
"""
logger(
f"Updating the table with router_id {packet_entry.router_id}",
is_debug=True,
)
table_entry: RouteEntry = table[packet_entry.router_id]
if next_hop == table_entry.next_hop and new_metric != INFINITY:
table_entry.update_timeout_time(table.timeout_delta)
if (
next_hop == table_entry.next_hop and new_metric != table_entry.metric
) or new_metric < table_entry.metric:
adopt_route(
table,
table_entry,
new_metric,
next_hop,
sock,
packet_entry.router_id,
)
elif new_metric == INFINITY:
# nothing happens if the entry's existing metric is `INFINITY`
if table_entry.metric != INFINITY:
pool.submit(deletion_process, table, packet_entry.router_id)
elif new_metric == table_entry.metric and next_hop != table_entry.next_hop:
# Adding a check for `next_hop` means that the entry will not be
# updated if its the same as the old entry.
# If the timeout for the existing route is at least halfway to the
# expiration point, switch to the new route.
time_diff: timedelta = table_entry.timeout_time - datetime.now()
half_time = table.timeout_delta / 2
if time_diff.seconds >= half_time:
adopt_route(
table,
table_entry,
new_metric,
next_hop,
sock,
packet_entry.router_id,
)
else:
# The packet_entry is no better than the current route
pass
def process_entry(
table: RoutingTable,
packet_entry: ResponseEntry,
packet: ResponsePacket,
port: int,
sock: socket,
):
"""Processes a single entry from a received packet."""
logger("Processing an entry", is_debug=True)
if not validate_entry(table, packet_entry):
# Ignores invalid entries
return
# Update the metric
new_metric = packet_entry.metric + table[packet.sender_router_id].metric
if new_metric > INFINITY:
new_metric = INFINITY
if packet_entry.router_id in table:
update_table(
table, packet_entry, new_metric, packet.sender_router_id, sock
)
else:
add_route(
table, packet_entry, new_metric, packet.sender_router_id, sock
)
def get_packets(
sockets: List[socket]
) -> List[Tuple[ResponsePacket, int, socket]]:
"""
Gets a tuple of the received packets from the input sockets, and their
associated port numbers and sockets.
Returns a list of tuples, where each tuple is (ResponsePacket, port, sock).
"""
read: List[socket]
read, _, _ = select(sockets, [], [], 1)
packets = []
for sock in read:
_, port = sock.getsockname()
# The buffer size is bigger than the maximum packet size.
raw_packet, client_address = sock.recvfrom(1024)
packet = read_packet(raw_packet)
packets.append((packet, port, sock))
logger(
f"Received packet from router_id: {packet.sender_router_id} | "
f"input port {port}",
is_debug=True,
)
return packets
def add_discovered(table: RoutingTable, packet: ResponsePacket, sock: socket):
"""
Adds entries discovered implicitly from the packet itself into the routing
table.
"""
if packet.sender_router_id not in table.config_table:
logger(
f"router_id {packet.sender_router_id} is not in "
"the config file.",
is_debug=True,
)
return
metric = table.config_table[packet.sender_router_id].cost
# The idea is generally the same as adding a new route into the table, even
# if updating an entry, because we'll want to completely wipe the entry and
# timers.
fake_packet_entry = ResponseEntry(AF_INET, packet.sender_router_id, metric)
add_route(table, fake_packet_entry, metric, packet.sender_router_id, sock)
def input_processing(table: RoutingTable, sockets: List[socket]):
"""
The processing is the same, no matter why the Response was generated.
"""
for packet, port, sock in get_packets(sockets):
router_id = packet.sender_router_id
if validate_packet(table, packet):
for entry in packet.entries:
process_entry(table, entry, packet, port, sock)
# The following adds entries if the packet sender's `router_id` is
# inside the config file.
if router_id in table.config_table:
if router_id not in table:
# If the sender's `router_id` isn't in the routing table, add
# it.
add_discovered(table, packet, sock)
elif table.config_table[router_id].cost <= table[router_id].metric:
# Actually updates the entry. The underlying idea is the same
# as adding a newly discovered route.
add_discovered(table, packet, sock)
elif (
table[router_id].next_hop not in table
or table[table[router_id].next_hop].metric == INFINITY
):
# Let the next_hop for the router be R. If R isn't in the
# routing table, or if R has a metric of infinity, update
# the entry with the information inferred from the packet.
add_discovered(table, packet, sock)
else:
# There's no changes here, thus update the timeout timer.
table[router_id].update_timeout_time(table.timeout_delta)
logger(str(table))