diff --git a/TINF-Praxis-Bestaetigung.docx b/TINF-Praxis-Bestaetigung.docx new file mode 100644 index 0000000..3651d50 Binary files /dev/null and b/TINF-Praxis-Bestaetigung.docx differ diff --git a/abk.tex b/abk.tex index 50bc9f3..3f1e518 100644 --- a/abk.tex +++ b/abk.tex @@ -7,7 +7,7 @@ \chapter*{Abkürzungsverzeichnis} % chapter*{..} --> keine Nummer, kein "Kapitel" % Nicht ins Inhaltsverzeichnis -% \addcontentsline{toc}{chapter}{Akürzungsverzeichnis} % Damit das doch ins Inhaltsverzeichnis kommt +\addcontentsline{toc}{chapter}{Akürzungsverzeichnis} % Damit das doch ins Inhaltsverzeichnis kommt % Hier werden die Abkürzungen definiert \begin{acronym}[DHBW] @@ -40,6 +40,9 @@ \acro{OSPF}[OSPF]{Open Shortest Path First} \acro{ASN}[ASN]{Autonome System Nummer} \acro{JSON}[JSON]{JavaScript Object Notation} + \acro{SSH}[SSH]{Secure Shell} + \acro{TTL}[TTL]{Time to live} + \acro{DRY}[DRY]{Don't repeat yourself} diff --git a/bericht.pdf b/bericht.pdf index c5d33fe..e48cbcd 100644 Binary files a/bericht.pdf and b/bericht.pdf differ diff --git a/bericht.sty b/bericht.sty index aa3b286..f342959 100644 --- a/bericht.sty +++ b/bericht.sty @@ -179,7 +179,7 @@ Vgl.\cite{\thefield{entrykey}}} commentstyle=\color{olive}, keywordstyle=\color{magenta}, numberstyle=\tiny\color{lightgray}, - stringstyle=\color{violet}, + stringstyle=\color{orange}, basicstyle=\ttfamily\footnotesize, breakatwhitespace=false, breaklines=true, diff --git a/bericht.tex b/bericht.tex index 59f5136..38a47a2 100644 --- a/bericht.tex +++ b/bericht.tex @@ -140,7 +140,7 @@ Gutachter der Studienakademie & \BetreuerDHBW \\ \end{titlepage} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - +\pagenumbering{Roman} \input{erklaerung.tex} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -163,12 +163,16 @@ Gutachter der Studienakademie & \BetreuerDHBW \\ \newpage \tableofcontents % Inhaltsverzeichnis hier ausgeben \listoffigures % Liste der Abbildungen +\addcontentsline{toc}{chapter}{Abbildungsverzeichnis} \listoftables % Liste der Tabellen +\addcontentsline{toc}{chapter}{Tabellenverzeichnis} \lstlistoflistings % Liste der Listings -\listofequations % Liste der Formeln +\addcontentsline{toc}{chapter}{Liste der Code Snippets} % Jetzt kommt der "eigentliche" Text \include{abk} % Abkürzungsverzeichnis + +\pagenumbering{arabic} \include{kapitel1} \include{kapitel2} \include{kapitel3} diff --git a/kapitel4.tex b/kapitel4.tex index 911bd93..6cdff8a 100644 --- a/kapitel4.tex +++ b/kapitel4.tex @@ -122,20 +122,638 @@ Mit seiner intelligenten Konvertierung und Verwaltung von Routen durch \ac{BGP} \subsection{Generieren der Config Files für Bird} %TODO RNDMISC-410 -Um die Routen an den Bird Routing Daemon übermitteln zu können, müssen diese erst in eine für Bird verständliche Konfigurationsdatei umgewandelt werden. -%Warum verwenden wir Jinja und wie funktionierts? +Für den einfachen Umgang mit Routen wurde für die \verb|routes| Variable eine Python Dataclass angelegt, welche das IP-Präfix, die IP-Version und eine Liste der gesetzen \ac{BGP}-Communities enthält. -\subsubsection{Integrität der Konfigurationsdatei sicherstellen} %TODO RNDMISC-420 +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:route-dataclass}, + captionpos=b, % Caption unter den Code setzen + caption={Python Dataclass für Routen Objekte}] +@dataclass +class Route: + prefix: str + encode: str + communities: list[str] + + def __init__(self, prefix="", encode="", communities=[]): + self.prefix = prefix + self.encode = encode + self.communities = communities + + def __str__(self): + return f"{self.encode} {self.prefix} {self.communities}" + + def decode_prefix(self): + self.prefix = self.prefix.replace("_", "/") + + def encode_prefix(self): + self.prefix = self.prefix.replace("/", "_") + + def get_communities(self): + communities = self.communities + return list(map(lambda com: com.replace(":", ","), communities)) +\end{lstlisting} + +Neben den genannten Feldern, kann die Dataclass auch noch Methoden zur verarbeitung der Felder, ähnlich wie eine normale Klasse beinhalten. +Besonders zu betonen ist hier die \verb|get_communities| Methode, welche die Communities in ein von Bird akzeptiertes Format umwandelt. + +\newpage + +Die in Consul gespeicherten Routen werden dann periodisch von Consul über dessen eigene \ac{API} abgefragt. +Hierfür ist die \verb|parse_consul_values| Methode zuständig. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:parse-consul-values}, + captionpos=b, % Caption unter den Code setzen + caption={parse\_consul\_values Methode}] +def parse_consul_values(values, watched_prefix) -> (list[Route], list[Route]): + if not values: + return [], [] + + v4_routes = [] + v6_routes = [] + + for entry in values: + route_entry = Route() + route_entry.prefix = entry["Key"].split(watched_prefix)[1] + json_communities = entry["Value"].decode("utf-8") + route_entry.communities = json.loads(json_communities)["communities"] + + route_entry.encode, route_entry.prefix = route_entry.prefix.split("/") + route_entry.decode_prefix() + + if route_entry.encode == "IPv4": + v4_routes.append(route_entry) + + elif route_entry.encode == "IPv6": + v6_routes.append(route_entry) + return v4_routes, v6_routes +\end{lstlisting} + +Im Laufe der Methode werden die abgefragten Einträge in Routenobjekte umgewandelt. +Um herauszufinden um welche IP Version es sich bei der Route handelt, wird der entsprechende Key des Pfades ausgelesen, da dieser die IP Version mit im Namen trägt. +Als Resultat gibt die Methode ein Tupel zurück, wobei eines die IPv4 Routen und das andere die IPv6 Routen sind. +Diese Aufteilung ist notwendig, da die in Debian 11 mitgelieferte Version von Bird eine klare Auftrennung dieser fordert. +Neuere Versionen von Bird können auch mit beiden IP Versionen gleichzeitig umgehen. + +Durch die Auftrennung der beiden IP-Versionen, muss auch die Konfigurationsdatei für Bird, zweimal generiert werden. +Mit der Methode \verb|generate_bird_files| werden Umgebungsvariablen geladen, welche für die Generierung der Konfiguration benötigt werden. +Neben diesen übergibt die Methode auch die Routen als Parameter weiter. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:generate-bird-files}, + captionpos=b, % Caption unter den Code setzen + caption={generate\_bird\_files Methode}] +def generate_bird_files(v4_routes, v6_routes, pybird, pybird6): + gen = BirdConfigGenerator() + click.echo("Generating and committing config files") + gen.bird_config( + v4_routes, + "route_template.j2", + "../config/bird", + "v4.conf", + os.getenv("ROUTER_ID"), + os.getenv("LOCAL_AS"), + os.getenv("REMOTE_AS"), + os.getenv("BGP_NEIGHBOR"), + "ANEXIA Route Injection v4", + ) + + gen.bird_config( + v6_routes, + "route_template.j2", + "../config/bird", + "v6.conf", + os.getenv("ROUTER_IDv6"), + os.getenv("LOCAL_AS"), + os.getenv("REMOTE_AS"), + os.getenv("BGP_NEIGHBORv6"), + "ANEXIA Route Injection v6", + ) + + pybird.commit_config() + pybird6.commit_config() + click.echo("Done") +\end{lstlisting} + +\newpage + +Die Methode \verb|bird_config|, welche um eine gute Struktur zu wahren zu einer gesonderten Datei und Klasse angehört, ruft die \verb|get_communities| Methode der Route Dataclass auf, um die Routen in einer von Bird lesbares Format zu wandeln. +Des Weiteren wird hier die tatsächliche Konfiguration auf das Dateisystem geschrieben. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:bird-config}, + captionpos=b, % Caption unter den Code setzen + caption={bird\_config Methode}] +def bird_config( + self, + unconverted_routes, + route_template, + config_path, + config_name, + router_id=None, + local_as=None, + remote_as=None, + bgb_neighbor=None, + description=None, +): + converted_routes = [] + for route in unconverted_routes: + route.communities = route.get_communities() + converted_routes.append(route) + + target_file = self.__prepare_config_path(config_path, config_name) + self.__write_config_file( + target_file, + converted_routes, + route_template, + router_id, + local_as, + remote_as, + bgb_neighbor, + description, + ) +\end{lstlisting} + +Um die Routen an den Bird Routing Daemon übermitteln zu können, müssen diese erst in eine für Bird verständliche Konfigurationsdatei umgewandelt werden. +Zur Realisierung wird die Jinja2 Templating Engine verwendet, da diese die Möglichkeit schafft, alle Eigenschaften des Injectors dynamisch zu konfigurieren. +Somit kann der tatsächliche Code aller Injectoren identisch sein, und Variable Eigenschaften wie z.B. die \verb|router_id| oder der \ac{BGP}-Peering Nachbar können beim Ausrollen festgelegt werden. + +\begin{lstlisting}[%language=tex, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:route-template}, + captionpos=b, % Caption unter den Code setzen + caption={Jinja Template zur Konfigurationsgenerierung}] +protocol static injected_routes { + {% for route in routes %} + route {{route.prefix}} via {{router_id}} { + {% for community in route.communities %} + bgp_community.add(({{community}})); + {% endfor %} + }; + {% endfor %} +} + +protocol bgp Route_Injection { +description "{{description}}"; + local as {{local_as}}; + neighbor {{bgp_neighbor}} as {{remote_as}}; +next hop self; +export filter { + if proto = "injected_routes" then accept; + reject; + }; +} +\end{lstlisting} + +In diesem Template finden sich einige Variablen + +\begin{itemize} + \item \verb|routes| (Python Liste mit Routen Elementen) + \item \verb|router_id| (IPv4 Addresse des Injectors) + \item \verb|local_as| (Lokales \ac{ASN}) + \item \verb|remote_as| (Nachbar \ac{ASN}) + \item \verb|bgp_neighbor| (Nachbar \ac{BGP}-Router IPv4 Adresse) + \item \verb|description| (Beschreibung des Protokolls) +\end{itemize} + +wieder, welche entweder im Code oder dynamisch beim Ausrollen, also ausrollen des Injectors gesetzt werden müssen. +Jinja kann auch mit Listen und verschachtelten Listen umgehen, was bei der \verb|routes| Variable zum Einsatz kommt. +Jinja kann dann über die Liste der Routenobjekte iterieren und für jede Route einen gesonderten Eintrag mit den jeweiligen \ac{BGP}-Communities erstellen. +Folglich ein Beispiel einer möglichen Konfiguration. \acp{ASN} und \verb|router_id|s können hier entweder als Umgebungsvariable oder von einer .env Datei geladen werden. +Die Routen werden dynamisch während der Programmlaufzeit angegeben, konvertiert und konfiguriert. + +\begin{lstlisting}[%language=tex, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:bird-configuration-example}, + captionpos=b, % Caption unter den Code setzen + caption={Beispiel einer Konfigurationsdatei}] +protocol static injected_routes { + route 1.1.1.1/32 via 172.20.0.5 { + bgp_community.add((47147,3200)); + bgp_community.add((12345,12345)); + }; +} + +protocol bgp Route_Injection { +description "ANEXIA Route Injection v4"; + local as 64701; + neighbor 172.20.0.6 as 65001; +next hop self; +export filter { + if proto = "injected_routes" then accept; + reject; + }; +\end{lstlisting} + +\newpage + +Somit können nun Konfigurationsdateien für Bird erstellt werden. +Sollte jedoch während dem Rendern des Templates ein Fehler auftreten, kann es passieren, dass eine inkorrekte oder gar keine Konfigurationsdatei generiert wird. +Dies könnte einen negativen Einfluss auf die Operation des Service haben und muss somit verhindert werden. + +Um das Problem zu verhindern, wird die Konfiguration erst in eine temporäre Datei geschrieben. +Wenn dies erfolgreich war, dann wird die temporäre Datei umbenannt und in das echte Konfigurationsverzeichnis geschoben. +Da hier eine Datei überschrieben statt angepasst wird, gehen Dateiberechtigungen verloren und müssen neu gesetzt werden. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:write-config-file}, + captionpos=b, % Caption unter den Code setzen + caption={\_\_write\_config\_file Methode}] +def __write_config_file( + self, + target_path, + routes, + template, + router_id=None, + local_as=None, + remote_as=None, + bgp_neighbor=None, + description=None, +): + template = self.env.get_template(template) + with NamedTemporaryFile(delete=False, mode="w") as conf: + conf.write( + template.render( + routes=routes, + router_id=router_id, + local_as=local_as, + remote_as=remote_as, + bgp_neighbor=bgp_neighbor, + description=description, + ) + ) + try: + os.chmod(conf.name, 0o660) + shutil.move(conf.name, target_path) + except Exception as e: + echo(e) + os.remove(conf.name) +\end{lstlisting} + +\newpage \subsection{Status der Routen von Bird abfragen} %TODO RNDMISC-361 \subsubsection{Evaluation der pybird Bibliothek} -\subsection{Bird und Bird6 aufteilen} %TODO RNDMISC-362 +Da die entwickelte \ac{API} über einen Status Endpunkt verfügt, welcher letztendlich von der Anexia Engine abgerufen wird, muss auch der Injektor die benötigten Statusinformationen zur Verfügung stellen. +Hierfür wurde evaluiert, welche Python Bibliothek sich am besten zu diesem Zwecke eignet. + +Die Entscheidung für die Verwendung der `pybird` Bibliothek wurde aus folgenden Gründen getroffen: + +\begin{enumerate} + \item Funktionalität: Die `pybird` Bibliothek wurde speziell dafür entwickelt mit dem Bird Routing Daemon zu interagieren. + \item Direkte Socket Anbindung: `pybird` unterstützt die direkte Kommunikation mit dem Bird Control Socket, was eine erleichterte Kommunikation ermöglicht. + \item Aktualisierung und Wartung: Da die `pybird` Bibliothek aktiv gepflegt wird, kann sichergestellt werden, dass sie auch mit zukünftigen Versionen des Bird Routing Daemons kompatibel sein wird. + Des Weiteren kann so auch sichergestellt werden, dass das Route Injection Project sich auch in der Zukunft noch auf diese Bibliothek verlassen kann. + \item Open-Source: Durch den offenen Quellcode, kann sichergestellt werden, dass der Code keine Malware/Spyware enthält. + Sollte es nötig sein, kann der Quellcode der Bibliothek geforked, und auf die Bedürfnisse der Anexia angepasst werden. +\end{enumerate} + +Über die Methode \verb|get_routes| der \verb|PyBird| Klasse können die von Bird übernommenen Routen abgefragt werden. +Als Parameter kann das Präfix der Route angegeben werden, sodass die Ausgabe auf nur dieses Präfix beschränkt wird. +Pybird gibt die Ausgabe dann in folgendem Format zurück: + +\begin{lstlisting}[language=tex, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:pybird-raw-output}, + captionpos=b, % Caption unter den Code setzen + caption={Unverarbeitete Ausgabe von Pybird}] +[{'community': '65535:65281', + 'prefix': '1.2.3.4/32', + 'peer': '172.20.0.3', + 'interface': 'eth0', + 'source': 'injected_routes', + 'time': '13:37:47'}] +\end{lstlisting} + +Von dieser Ausgabe wird jedoch nur der Teil, welcher die Communities betrifft benötigt. +Folglich muss die Ausgabe noch im Code angepasst werden. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:respond-state-to-consul}, + captionpos=b, % Caption unter den Code setzen + caption={respond\_state\_to\_consul Methode}] +def respond_state_to_consul( + consul: ckv, pybird: PyBird, route: Route, injector_id: str +) -> None: + state = pybird.get_routes(prefix=route.prefix) + route.encode_prefix() + try: + actual_communities = state[0].get("community", "").split(" ") + except IndexError: + actual_communities = [] + expected_communities = list(route.communities) + state = get_bird_communities(expected_communities, actual_communities) + state = json.dumps({"communities": state}) + try: + consul.kv.put( + f"v1/state/{injector_id}/{route.encode}/{route.prefix}", + state, + ) + except requests.exceptions.ConnectionError: + click.echo("Lost consul while reporting route :c") + return + route.decode_prefix() + click.echo(f"Route {route.prefix} with state {actual_communities} pushed to consul") +\end{lstlisting} + +Um den Status zu bestimmen, werden die Communities, welche im Routenobjekt abgespeichert sind, mit den Communities welche von Bird zurückgegeben wurde verglichen. +Stimmen diese überein, so kann davon ausgegangen werden, dass Bird alle Communities akzeptiert hat und an den Nachbar Router übermitteln kann. +Sollte es Abweichungen zwischen den Communities geben, bedeutet dies, dass noch nicht alle Communities von Bird akzeptiert wurden. +Als Folge dessen werden auch nur die aktuell in Bird eingetragen Communities zurück an Consul übermittelt. +Die \ac{API}-Komponente des Route Injection Service fragt dann den in Consul eingetragenen Status ab und bestimmt dann selbst, ob der gesamte Prozess erfolgreich, noch im Gange oder fehlerhaft war. +Dies wird dann von der Anexia Engine interpretiert und ist für den Nutzer sichtbar. \subsection{Realisierung des Heartbeats} -\subsection{Emergency-Mode implementieren} %TODO RNDMISC-363 +Um sicherzustellen, dass die API den aktuellen Status der online verfügbaren Injektoren erfassen kann, verwenden die Injektoren ein sogenanntes `Heartbeat`-System, das seine Aktivität in Consul signalisiert. +Dieses Heartbeat wird in Form eines Wertes (Value) in Consul gemeldet. +Dieser Prozess ermöglicht es der API, den Zustand der einzelnen Injektoren zu überwachen und sicherzustellen, dass sie ordnungsgemäß funktionieren. + +Jeder Injektor meldet seinen Status durch das Schreiben eines Wertes (Value) in einen spezifischen Schlüssel-Wert-Pfad in Consul. +Dieser Pfad lautet: \verb|v1/state//heartbeat|. +Hierbei steht \verb|| für die eindeutige Kennung des Injektors. +Der Wert (Value), der in den oben genannten Schlüssel-Wert-Pfad geschrieben wird, hat den Inhalt `\{\}`, was auf ein leeres JSON-Objekt hinweist. +Dieses leere Objekt dient als Platzhalter und signalisiert der API, dass der Injektor aktiv ist und seinen Heartbeat meldet. +Der gemeldete Wert (Value) hat eine \ac{TTL} von 10 Sekunden. +Dies bedeutet, dass nachdem der Injektor seinen Heartbeat gemeldet hat, der Wert für 10 Sekunden in Consul bestehen bleibt. +Wenn innerhalb dieses Zeitraums keine weiteren Heartbeats gemeldet werden, wird der Wert automatisch aus Consul entfernt. + +Durch das Heartbeat-System kann die API regelmäßig aktualisierte Informationen erhalten, welche Injektoren online und funktionsfähig sind. + +\vspace{1cm} +Um den Heartbeat im Programmcode möglichst modular zu realisieren wurde hierfür eine eigene Methode erstellt. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:create-heartbeat}, + captionpos=b, % Caption unter den Code setzen + caption={create\_heartbeat Methode}] +def create_heartbeat(consul, injector_id): + session_id = consul.session.create(behavior="delete", ttl=10) + consul.kv.put( + key=f"v1/state/{injector_id}/heartbeat", value="{}", acquire=session_id + ) + return session_id +\end{lstlisting} + +Nach dem initialen Anlegen des Heartbeateintrages wird dieser alle fünf Sekunden erneuert und sicherzustellen, dass die \ac{TTL} des Eintrages nicht abläuft. + +\subsection{Emergency-Mode} %TODO RNDMISC-363 + + +Um sicherzustellen, dass der Route Injection Service auch in Szenarien von Netzwerkproblemen zwischen der \ac{API} und den Injektoren effizient arbeiten kann, sei es für das Hinzufügen, Ändern oder Löschen von Routen, wurde eine maßgebliche Funktion eingeführt, die als Emergency-Mode, bzw. Notfallmodus bekannt ist. +Diese Funktion wurde entwickelt, um direkten Zugriff auf die Injektoren zu ermöglichen und Routenverwaltungsvorgänge über die Kommandozeile durchzuführen. +Der Emergency-Mode fungiert als eine Art Sicherheitsvorkehrung, die sicherstellt, dass die Verfügbarkeit und Funktionalität des Dienstes aufrechterhalten werden kann, selbst wenn die übliche Kommunikation zwischen der \ac{API} und den Injektoren temporär gestört ist. +Der Namensteil `Mode` lässt vermuten, dass es sich um einen tatsächlichen Operationsmodus handelt. +Dies ist allerdings nicht ganz korrekt. +Der Emergency-Mode ist eher als Funktionalitätserweiterung zu sehen und kann selbst dann aktiviert werden, wenn die Kommunikation zwischen \ac{API} und Injector intakt ist. +Dies ist insbesondere nützlich, wenn dringende Änderungen an den Routingeinstellungen erforderlich sind, die nicht auf die normale Kommunikation warten können. +Um Zugriff auf die Kommandozeile zu erhalten, muss ein Nutzer sich über \ac{SSH} auf den Injector einloggen. +Firmeninterne Automatismen stellen sicher, dass nur befugte Nutzer Zugriff auf das System haben. + + +Zur Gewährleistung der Integrität des Service müssen die vom Nutzer eingegebene Routen validiert und auf Ihre Korrektheit überprüft werden. +In der Regel wird dies von der \ac{API} übernommen, jedoch werden im Notfallmodus die Routen direkt in den Injector eingespeist, und die Validierung der \ac{API} wird umgangen. +Daher muss diese vom Injector selbst durchgeführt werden. + +\newpage + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:validate-route}, + captionpos=b, % Caption unter den Code setzen + caption={Methode zur Validierung von Routen}] +def validate_route(prefix: str, communities=None) -> Route: + route = Route() + try: + route.prefix = str(ipaddress.ip_network(prefix)) + except ValueError: + raise click.exceptions.BadParameter("Route prefix is invalid") + if ":" in route.prefix: + route.encode = "IPv6" + else: + route.encode = "IPv4" + if communities: + communities = communities.split(",") + for community in communities: + community_parts = community.split(":") + if len(community_parts) != 2: + raise click.exceptions.BadParameter( + f"{community} is not a valid BGP community" + ) + try: + if not int(community_parts[0]) in range(1, 65535) or not int( + community_parts[1] + ) in range(1, 65535): + raise click.exceptions.BadParameter( + f"{community} is not a valid BGP community" + ) + except ValueError: + raise click.exceptions.BadParameter( + f"{community} contains invalid integer value" + ) + route.communities = list(communities) + return route +\end{lstlisting} + +Der Zweck ist, BGP-Routen, primär in Bezug auf deren Präfixe und Communities zu validieren. +Die Methode akzeptiert ein Präfix als obligatorisches Argument und optional eine Liste von Communities als Zeichenfolge. +Das Hauptziel dieser Funktion ist es, sicherzustellen, dass die angegebenen Informationen den \ac{BGP}-Anforderungen entsprechen und gültig sind. + +Zuerst wird ein neues Routenobjekt erstellt, das als Container für die validierten Daten dient. +Die Funktion versucht dann, den übergebenen Präfix als IP-Netzwerk zu interpretieren. +Bei einer ungültigen Eingabe wird eine `BadParameter`-Exception ausgelöst. + +Das Präfix wird analysiert, um festzustellen, ob es sich um ein IPv4- oder IPv6-Präfix handelt. +Dies wird im `encode`-Attribut des Routenobjekts vermerkt. +Im Fall von übergebenen Communities werden diese analysiert und validiert. +Jede Community wird auf ihre Struktur überprüft, und die einzelnen Teile werden auf ihre Gültigkeit im Hinblick auf \ac{ASN} und Wertigkeit geprüft. +Fehlerhafte Communities führen zu entsprechenden `BadParameter`-Exceptions. + +Abschließend werden die validierten Informationen, einschließlich Präfix und Communities, im Routenobjekt gespeichert. +Die Funktion gibt dieses Objekt zurück, das nun die validierten Daten enthält. + +Zur Vereinfachung der Interaktionen mit der Kommandozeile wird die Bibliothek `click` verwendet. +Durch diese können Exceptions leicht an den Benutzer übermittelt werden, und Tests können einfach gestaltet werden. + +Der Operator welcher letztendlich den Emergency Mode bedienen wird, hat zwei Eingabemöglichkeiten: + +\begin{itemize} + \item add-route + \item delete-route +\end{itemize} + +Wobei `<>` für Platzhalter des entsprechenden Parameters stehen. +Eine Möglichkeit, schon existierende Routen zu updaten bietet der Emergency Mode nicht. +Routen welche über den Emergency Mode hinzugefügt wurde, haben immer Vorrang gegenüber Routen, welche über Consul geladen wurden. +Eine weitere Anforderung an den Emergency Mode war, dass Routen auch nach Reboot des Injectors erhalten bleiben. +Dies forderte, dass Routen auf einer Weise im Dateisystem erhalten werden. +Um dies zu Realisieren bestünde die Möglichkeit eine Datenbank wie `sqlite` zu nutzen. +Eine einfachere Lösung dieses Problems war es jedoch, die Routen als \ac{JSON} in eine Datei zu schreiben. +Die schon bei den Konfigurationsdateien für Bird, wurden IPv4 und IPv6 aus demselben Grund getrennt. + +Um die Konsistenz und Integrität dieser Dateien, auch `Emergency Files` genannt zu gewährleisten, wurde ein Filelock gesetzt. +Zur Vermeidung des Dirty read Problems, welches in der Vorlesung Datenbanken erläutert wurde, wurde das Filelock sowohl für Schreib- als auch für Lesevorgänge gesetzt. +So kann ein zweiter Prozess das Emergency File erst lesen, wenn der erste Prozess den Schreibvorgang abgeschlossen hat. +Dies dient nicht nur zur Mehrbenutzersynchronisation von mehreren Menschen, sondern hauptsächlich, dass der Hauptprozess nicht versucht das Emergency File zu lesen, während ein Operator mittels des Emergency Mode Änderungen vornimmt. + +Da Python, beziehungsweise die benutze \ac{JSON} Bibliothek Probleme damit hatte verschachtelte \ac{JSON}s zu de- und enkodieren, wurde eine weitere Dataclass angelegt. +Diese Dataclass dient nur als Container, um eine Liste an Routenobjekten anzulegen. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:route-container}, + captionpos=b, % Caption unter den Code setzen + caption={Route Container Dataclass}] +@dataclass_json +@dataclass +class RouteContainer: + routes: list[Route] + + def __init__(self, routes): + self.routes = routes + + def __str__(self): + return f"{self.routes}" +\end{lstlisting} + +Zu Beginn des Programmstarts werden die Pfade der Lockfiles und Emergency Files innerhalb des Docker Containers festgelegt: + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:path-declaration}, + captionpos=b, % Caption unter den Code setzen + caption={Deklaration der Dateipfade}] +emergency_file_v4 = "/var/lib/route_injector/emergency_route_v4.json" +emergency_file_v6 = "/var/lib/route_injector/emergency_route_v6.json" +lock_file_v4 = "/var/lib/route_injector/emergency_route_v4.lock" +lock_file_v6 = "/var/lib/route_injector/emergency_route_v6.lock" +\end{lstlisting} + +\newpage + +Die Methode welche beim Aufruf von \verb|add-route| über die Kommandozeile aufgerufen wird lässt sich wie folgt darstellen: + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:add-route}, + captionpos=b, % Caption unter den Code setzen + caption={add\_route Methode}] +@click.argument("communities") +@click.argument("prefix") +@cli.command() +def add_route(prefix, communities): + route = validate_route(prefix, communities) + emergency_file = emergency_file_v4 + lockfile = FileLock(lock_file_v4) + if route.encode == "IPv6": + emergency_file = emergency_file_v6 + lockfile = FileLock(lock_file_v6) + with lockfile.acquire(): + current_routes = read_emergency_file(emergency_file) + new_routes = find_and_remove_in_list(current_routes, route) + new_routes.append(route) + route_container = RouteContainer(new_routes) + write_emergency_file(route_container, emergency_file) +\end{lstlisting} + +Zuerst wird über den erwähnten Validierungsprozess sichergestellt, dass die vom Nutzer eingegeben Route eine valide Route ist. +Über eine if Abfrage wird geprüft, ob das Präfix der eingegebenen Route ein IPv6 Präfix ist. +Ist das der Fall, dann wird das entsprechende Emergency File und Lockfile einer Variablen zugewiesen. +Anschließend wird das Filelock auf das entsprechende Emergency File gesetzt, um sicherzustellen, dass keine weiteren Prozesse auf das File zugreifen können. +Im Folgenden werden die schon im Emergency File enthaltenen Routen mit den neu hinzugefügten verglichen. +Sollte eine Route hinzugefügt werden, wessen Präfix schon im aktuellen Emergency File enthalten ist, wird diese über die \verb|find_and_remove_in_list| entfernt. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:find-and-remove-in-list}, + captionpos=b, % Caption unter den Code setzen + caption={find\_and\_remove\_in\_list Methode}] +def find_and_remove_in_list(route_list: list, list_element: Route): + for element in route_list: + if element.prefix == list_element.prefix: + route_list.remove(element) + return route_list +\end{lstlisting} + +Die neue Route wird danach der Liste von Routen hinzugefügt, und über die RouteContainer Dataclass wieder zu einer verschachtelten \ac{JSON} konvertiert. +Zum Löschen von Routen aus den Emergency Files, gibt es die \verb|delete_route| Methode, welche sich maßgeblich dadurch unterscheidet, dass sie keine \ac{BGP}-Communities als Parameter benötigt, sondern lediglich das Routenpräfix. +Infolgedessen, fehlt in dieser Methode auch der Teil, welcher die neue Route der Routenliste hinzufügt, da hier nur die Route entfernt werden muss. + +Da das Lesen und Schreiben der Files mehrmals im Programmcode geschieht, wurde hierfür jeweils eine Methode geschrieben um Codeduplizierung möglichst zu vermeiden und das \ac{DRY} Prinzip einzuhalten. + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:read-emergency-file}, + captionpos=b, % Caption unter den Code setzen + caption={read\_emergency\_file Methode}] +def read_emergency_file(emergency_route_file: str) -> list: + if not os.path.exists(emergency_route_file): + return [] + with open(emergency_route_file, "r") as emergency_route: + json_routes = emergency_route.read() + routes_from_file = RouteContainer.from_json(json_routes).routes + return routes_from_file +\end{lstlisting} + +\begin{lstlisting}[language=python, + frame=single, % Ein Rahmen um den Code + framexleftmargin=15pt, % Rahmen link von den Zahlen + style=algoBericht, + label={lst:write-emergency-file}, + captionpos=b, % Caption unter den Code setzen + caption={write\_emergency\_file Methode}] +def write_emergency_file(routes: RouteContainer, emergency_route_file: str): + with NamedTemporaryFile(delete=False, mode="w") as tmp_emergency_route_file: + tmp_emergency_route_file.write(routes.to_json()) + try: + shutil.move(tmp_emergency_route_file.name, emergency_route_file) + except Exception as e: + click.echo(e) + os.remove(tmp_emergency_route_file.name) +\end{lstlisting} + +\newpage \section{Testen}