Skip to content

Commit df27db4

Browse files
Manuel-Polpgallino
andauthored
Support ARP protocol (lightly) (#216)
## Description This pr add support for using ARP protocol in simulatior. Although this is a more lightfull version of the protocol. First of all, as LANs are not implemented, there’s no limit for a device to send an _ARP request_ to, but only switches can propagate ARP packets. This means that if a host or a router receive an ARP packet which wasn’t meant for it, it will discard it. Even routers, which in reality have the ability to, either resolve the address requesting or to forward the packet to the corresponding device, as long as the device corresponds to the same LAN. Also, every device _ARP table_ is authomatically completed as the device connects to other devices. This will simplify the UX, as user will not have to see how the protocol works each time a recently device sends a packet to another. User will be able to see _ARP request_ and _responds_ packets by running the ARP program. ARP table entries can be deleted or modified manually by the user. ## Implementation details ### ARP table The tables are automatically populated each time a device is added to the network. Adding each entry to the new device's table and updating the corresponding entries in all existing devices would have a time complexity of O(n). To avoid this cost, the entries in the table (which is implemented as a `Map` in the code) are managed as follows: **(a)** If there is an entry with the device's IP address: - **(i)** If the `mac` field is an empty string, this indicates that the user manually deleted the entry. In this case, it is treated as if the entry does not exist in the ARP table, and the IP address cannot be resolved. - **(ii)** If the `mac` field is a non-empty string (presumably a MAC address), the entry was modified at some point—either manually by the user or via an ARP Request tool. In both cases, this entry is used to resolve the IP address. **(b)** If there is no entry with the device's IP address: This means the user has never modified the IP entry, either manually or through an ARP Request. We assume the entry exists and use the viewgraph to locate the device associated with the IP and, in turn, find its MAC address. From the user's perspective, the table will appear exactly as it would in a real-world scenario. --------- Co-authored-by: Pedro Gallino <[email protected]> Co-authored-by: Manuel Pol <[email protected]>
1 parent a5799a9 commit df27db4

27 files changed

+788
-154
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { DeviceId } from "../../types/graphs/datagraph";
2+
import { ViewGraph } from "../../types/graphs/viewgraph";
3+
import { ALERT_MESSAGES } from "../../utils/constants/alert_constants";
4+
import { CSS_CLASSES } from "../../utils/constants/css_constants";
5+
import { TOOLTIP_KEYS } from "../../utils/constants/tooltips_constants";
6+
import { Button } from "../basic_components/button";
7+
import { Table } from "../basic_components/table";
8+
import { ToggleButton } from "../basic_components/toggle_button";
9+
import { showError, showSuccess } from "./alert_manager";
10+
11+
export interface ArpTableProps {
12+
rows: string[][]; // Rows for the table
13+
viewgraph: ViewGraph; // ViewGraph instance for callbacks
14+
deviceId: DeviceId; // Device ID for callbacks
15+
}
16+
17+
export class ArpTable {
18+
private container: HTMLElement;
19+
private table: Table;
20+
private toggleButton: ToggleButton;
21+
22+
constructor(private props: ArpTableProps) {
23+
this.container = document.createElement("div");
24+
this.container.className = CSS_CLASSES.ROUTING_TABLE_CONTAINER;
25+
26+
const { onEdit, onRegenerate, onDelete } = this.setArpTableCallbacks();
27+
28+
// Create the regenerate button
29+
const regenerateButton = this.createRegenerateButton(onRegenerate);
30+
31+
const headers = {
32+
[TOOLTIP_KEYS.IP]: TOOLTIP_KEYS.IP,
33+
[TOOLTIP_KEYS.MAC_ADDRESS]: TOOLTIP_KEYS.MAC_ADDRESS,
34+
[TOOLTIP_KEYS.REGENERATE]: regenerateButton, // Add the regenerate button to the header
35+
};
36+
37+
this.table = new Table({
38+
headers: headers,
39+
fieldsPerRow: 2, // IP and MAC
40+
rows: props.rows,
41+
editableColumns: [false, true], // Make all columns non-editable
42+
onEdit: onEdit,
43+
onDelete: onDelete,
44+
tableClasses: [CSS_CLASSES.RIGHT_BAR_TABLE],
45+
});
46+
47+
this.toggleButton = new ToggleButton({
48+
text: TOOLTIP_KEYS.ARP_TABLE,
49+
className: CSS_CLASSES.RIGHT_BAR_TOGGLE_BUTTON,
50+
onToggle: (isToggled) => {
51+
const tableElement = this.table.toHTML();
52+
tableElement.style.display = isToggled ? "block" : "none";
53+
},
54+
tooltip: TOOLTIP_KEYS.ARP_TABLE,
55+
});
56+
57+
// Initially hide the table
58+
const tableElement = this.table.toHTML();
59+
tableElement.style.display = "none";
60+
61+
this.initialize();
62+
}
63+
64+
private initialize(): void {
65+
this.container.appendChild(this.toggleButton.toHTML());
66+
this.container.appendChild(this.table.toHTML());
67+
}
68+
69+
toHTML(): HTMLElement {
70+
return this.container;
71+
}
72+
73+
updateRows(newRows: string[][]): void {
74+
this.table.updateRows(newRows); // Update the table with new rows
75+
}
76+
77+
// Function to create the regenerate button
78+
private createRegenerateButton(
79+
onRegenerateCallback: () => void,
80+
): HTMLButtonElement {
81+
const regenerateButton = new Button({
82+
text: "🔄",
83+
classList: [CSS_CLASSES.REGENERATE_BUTTON],
84+
onClick: onRegenerateCallback,
85+
});
86+
87+
return regenerateButton.toHTML();
88+
}
89+
90+
private OnRegenerate(): void {
91+
const dataGraph = this.props.viewgraph.getDataGraph();
92+
93+
dataGraph.clearArpTable(this.props.deviceId);
94+
95+
const newTableData = dataGraph.getArpTable(this.props.deviceId);
96+
97+
if (!newTableData || newTableData.length === 0) {
98+
console.warn("Failed to regenerate ARP table.");
99+
showError(ALERT_MESSAGES.ARP_TABLE_REGENERATE_FAILED);
100+
return;
101+
}
102+
103+
// Convert ARP table entries to rows
104+
const newRows = newTableData.map((entry) => [entry.ip, entry.mac]);
105+
106+
this.updateRows(newRows);
107+
108+
showSuccess(ALERT_MESSAGES.ARP_TABLE_REGENERATED);
109+
}
110+
111+
private setArpTableCallbacks() {
112+
const onDelete = (row: number) => {
113+
// Obtener la tabla ARP actual
114+
const arpTable = this.props.viewgraph
115+
.getDataGraph()
116+
.getArpTable(this.props.deviceId);
117+
118+
// Validar que el índice es válido
119+
if (row < 0 || row >= arpTable.length) {
120+
console.warn(`Invalid row index: ${row}`);
121+
return false;
122+
}
123+
124+
// Obtener la IP correspondiente a la fila
125+
const ip = arpTable[row].ip;
126+
127+
// Eliminar la entrada de la tabla ARP usando la IP
128+
this.props.viewgraph
129+
.getDataGraph()
130+
.removeArpTableEntry(this.props.deviceId, ip);
131+
132+
return true;
133+
};
134+
135+
const onRegenerate = () => {
136+
console.log("Regenerating ARP table...");
137+
this.OnRegenerate();
138+
};
139+
140+
const onEdit = (row: number, _col: number, newValue: string) => {
141+
const isValidMac = isValidMAC(newValue);
142+
143+
if (!isValidMac) {
144+
console.warn(`Invalid value: ${newValue}`);
145+
return false;
146+
}
147+
148+
// Obtener la tabla ARP actual
149+
const arpTable = this.props.viewgraph
150+
.getDataGraph()
151+
.getArpTable(this.props.deviceId);
152+
153+
// Validar que el índice es válido
154+
if (row < 0 || row >= arpTable.length) {
155+
console.warn(`Invalid row index: ${row}`);
156+
return false;
157+
}
158+
159+
// Obtener la IP correspondiente a la fila
160+
const ip = arpTable[row].ip;
161+
162+
// Update the ARP table entry
163+
this.props.viewgraph
164+
.getDataGraph()
165+
.saveARPTManualChange(this.props.deviceId, ip, newValue);
166+
167+
return true;
168+
};
169+
170+
return { onEdit, onRegenerate, onDelete };
171+
}
172+
}
173+
174+
function isValidMAC(mac: string): boolean {
175+
// Expresión regular para validar direcciones MAC en formato estándar (XX:XX:XX:XX:XX:XX)
176+
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
177+
178+
const result = macRegex.test(mac);
179+
// Verificar si la dirección MAC coincide con el formato esperado
180+
if (!result) {
181+
showError(ALERT_MESSAGES.INVALID_MAC);
182+
}
183+
184+
return result;
185+
}

src/graphics/renderables/device_info.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { CSS_CLASSES } from "../../utils/constants/css_constants";
1414
import { BaseInfo } from "./base_info";
1515
import { ProgressBar } from "../basic_components/progress_bar";
1616
import { LabeledProgressBar } from "../components/labeled_progress_bar";
17+
import { ArpTable } from "./arp_table";
1718
import { Layer } from "../../types/layer";
1819

1920
export class DeviceInfo extends BaseInfo {
@@ -96,6 +97,8 @@ export class DeviceInfo extends BaseInfo {
9697
});
9798

9899
this.inputFields.push(routingTable.toHTML());
100+
101+
this.addDivider();
99102
}
100103

101104
addEmptySpace(): void {
@@ -117,6 +120,7 @@ export class DeviceInfo extends BaseInfo {
117120
parameters,
118121
);
119122
this.inputFields.push(parameterEditor.toHTML());
123+
this.addDivider();
120124
}
121125

122126
/**
@@ -140,6 +144,20 @@ export class DeviceInfo extends BaseInfo {
140144
);
141145
this.inputFields.push(labeledProgressBar.toHTML());
142146
}
147+
148+
addARPTable(viewgraph: ViewGraph, deviceId: number): void {
149+
const entries = viewgraph.getArpTable(deviceId);
150+
151+
const rows = entries.map((entry) => [entry.ip, entry.mac]);
152+
153+
const arpTable = new ArpTable({
154+
rows,
155+
viewgraph,
156+
deviceId,
157+
});
158+
159+
this.inputFields.push(arpTable.toHTML());
160+
}
143161
}
144162

145163
function getTypeName(device: ViewDevice): string {

src/graphics/renderables/program_info.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DeviceId } from "../../types/graphs/datagraph";
22
import { ViewGraph } from "../../types/graphs/viewgraph";
3+
import { DeviceType } from "../../types/view-devices/vDevice";
34
import { Layer, layerIncluded } from "../../types/layer";
45
import { TOOLTIP_KEYS } from "../../utils/constants/tooltips_constants";
56
import { Dropdown } from "../basic_components/dropdown";
@@ -29,6 +30,13 @@ export class ProgramInfo implements Renderable {
2930
this.withDropdown(TOOLTIP_KEYS.DESTINATION, devices);
3031
}
3132

33+
withDestinationIpDropdown(viewgraph: ViewGraph, srcId: DeviceId) {
34+
this.withDropdown(
35+
TOOLTIP_KEYS.IP_REQUEST,
36+
otherDevicesIp(viewgraph, srcId),
37+
);
38+
}
39+
3240
withDropdown(name: string, options: { value: string; text: string }[]) {
3341
const dropdown = new Dropdown({
3442
default_text: name,
@@ -62,3 +70,22 @@ function otherDevices(viewgraph: ViewGraph, srcId: DeviceId) {
6270
.filter((id) => id !== srcId)
6371
.map((id) => ({ value: id.toString(), text: `Device ${id}`, id }));
6472
}
73+
74+
function otherDevicesIp(viewgraph: ViewGraph, srcId: DeviceId) {
75+
return viewgraph
76+
.getDevices()
77+
.filter(
78+
(device) =>
79+
device.visible &&
80+
device.getType() !== DeviceType.Switch &&
81+
device.id !== srcId,
82+
)
83+
.map((device) => {
84+
if ("ip" in device) {
85+
const ipString = device.ip.toString();
86+
return { value: ipString, text: `${ipString} (Device ${device.id})` };
87+
}
88+
// Shouldn’t get here
89+
return { value: "", text: "" };
90+
});
91+
}

src/graphics/renderables/program_runner_info.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ export class ProgramRunnerInfo implements Renderable {
8383
private addRunningProgramsList() {
8484
this.runningProgramsTable = this.generateProgramsTable();
8585
this.inputFields.push(this.runningProgramsTable);
86-
this.inputFields.push(document.createElement("br"));
8786
}
8887

8988
private createProgramsTable(

src/graphics/renderables/routing_table.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ export class RoutingTable {
124124
else if (col === ROUTER_CONSTANTS.INTERFACE_COL_INDEX)
125125
isValid = isValidInterface(newValue);
126126
if (isValid) {
127-
viewgraph.getDataGraph().saveManualChange(deviceId, row, col, newValue);
127+
viewgraph
128+
.getDataGraph()
129+
.saveRTManualChange(deviceId, row, col, newValue);
128130
showSuccess(ALERT_MESSAGES.ROUTING_TABLE_UPDATED);
129131
}
130132
return isValid;

0 commit comments

Comments
 (0)