764 lines
37 KiB
TeX
764 lines
37 KiB
TeX
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% Descr: Vorlage für Berichte der DHBW-Karlsruhe, Ein Kapitel
|
|
%% Author: Prof. Dr. Jürgen Vollmer, vollmer@dhbw-karlsruhe.de
|
|
%% $Id: kapitel1.tex,v 1.24 2020/03/13 16:02:34 vollmer Exp $
|
|
%% -*- coding: utf-8 -*-
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
\chapter{API Komponente}
|
|
|
|
\section{Aufgaben}
|
|
|
|
Die \ac{API} ist die Schnittstelle des Service und außen stehenden Technologien wie der Anexia Engine.
|
|
Ihre Hauptaufgabe besteht darin, eine strukturierte Interaktionsmöglichkeit zu bieten, die es internen Benutzern über Systeme wie der Anexia Engine ermöglicht, \ac{BGP}-Routen mit zugehörigen \ac{BGP}-Communities in das Netzwerk zu injizieren.
|
|
Dies geschieht durch die Annahme von \ac{JSON}-Anfragen, die spezifische Informationen enthalten, nämlich IPv4- oder IPv6-Präfixe und die entsprechenden \ac{BGP}-Communities.
|
|
Die \ac{API} führt eine umfassende Validierung der eingehenden Daten durch, um sicherzustellen, dass die bereitgestellten Informationen korrekt und im erwarteten Format vorliegen.
|
|
Diese Validierung umfasst die Überprüfung der Richtigkeit der IP-Adressbereiche sowie die syntaktische Korrektheit der zugeordneten \ac{BGP}-Communities.
|
|
Durch diesen Schritt wird gewährleistet, dass nur gültige Informationen in das System eingebracht werden.
|
|
Die validierten Daten werden anschließend an Consul, über dessen eigene \ac{API} übermittelt.
|
|
Die Daten werden so abgelegt, dass der Injector einen erleichterten Zugriff hat.
|
|
|
|
\newpage
|
|
|
|
\section{Umsetzung}
|
|
|
|
Da die Konzeption und Implementierung der \ac{API} schon umfassend in der Projektarbeit T1000 erläutert wurde, wird auf eine Wiederholung dessen verzichtet.
|
|
In diesem Bericht wird lediglich die Implementierung des `Delete`-Endpunkts dargestellt, da dieser aus zeitlichen Gründen nicht mehr in den ersten beiden Praxisphase implementiert werden konnte, jedoch ein Grundbestandteil des entwickelten Service ist.
|
|
|
|
\vspace{1cm}
|
|
|
|
Die Implementierung eines `Delete`-Endpunkts in der API, mittels des Django Rest Frameworks, ermöglicht das Löschen von Routen aus dem System.
|
|
|
|
\begin{lstlisting}[language=python,
|
|
frame=single, % Ein Rahmen um den Code
|
|
framexleftmargin=15pt, % Rahmen link von den Zahlen
|
|
style=algoBericht,
|
|
label={lst:baserouteviewset},
|
|
captionpos=b, % Caption unter den Code setzen
|
|
caption={BaseRouteViewset Klasse}]
|
|
class BaseRouteViewSet(
|
|
CreateModelMixin,
|
|
ReadOnlyModelViewSet,
|
|
BaseRequestViewSet,
|
|
):
|
|
@action(detail=False, url_path=r"([A-Za-z-_/]*)status/(?P<task_info_id>[0-9a-z-]+)")
|
|
def status(self, request, task_info_id):
|
|
route_object = get_object_or_404(
|
|
self.serializer_class.Meta.model, task_info_id=task_info_id
|
|
)
|
|
propagate_status(route_object)
|
|
return super().status(request, task_info_id)
|
|
\end{lstlisting}
|
|
|
|
Der in Snippet 4.1 gezeigte Code stellt eine Mutterklasse dar, von welcher sowohl der `Create`, als auch `Delete`-Endpunkt erben.
|
|
Durch diese Klasse wird die Möglichkeit gegeben, von der Anexia Engine erwartete Endpunkte einfach zu implementieren, ohne dass sich ein Entwickler mit den Feinheiten dessen auseinandersetzen muss.
|
|
Da hier die \verb|CreateModelMixin| Klasse geerbt wird, stellt sich das \ac{DRF} automatisch ein 'POST'-Requests für diesen Endpunkt zu akzeptieren.
|
|
|
|
\newpage
|
|
|
|
\begin{lstlisting}[language=python,
|
|
frame=single, % Ein Rahmen um den Code
|
|
framexleftmargin=15pt, % Rahmen link von den Zahlen
|
|
style=algoBericht,
|
|
label={lst:deleterouteviewset},
|
|
captionpos=b, % Caption unter den Code setzen
|
|
caption={DeleteRouteViewset Klasse}]
|
|
class DeleteRouteViewSet(BaseRouteViewSet):
|
|
queryset = DeleteRoute.objects.all()
|
|
serializer_class = DeleteRouteSerializer
|
|
|
|
def perform_create(self, serializer):
|
|
super().perform_create(serializer)
|
|
delete_route(serializer.instance)
|
|
\end{lstlisting}
|
|
|
|
Die tatsächliche Implementierung fällt durch das Erben von der `BaseRouteViewSet` Mutterklasse sehr simpel aus.
|
|
Durch das Überschreiben der \verb|perform_create| Methode, welche vom \ac{DRF} zur Verfügung gestellt wird, kann diese als Hook benutzt werden um eigenen Code ausführen zu lassen.
|
|
Mit der Super Methode wird sichergestellt, dass die nicht überschriebene Ursprungsmethode von \verb|perform_create| ausgeführt wird.
|
|
Das \ac{DRF} erstellt dann einen Datenbankeintrag mit den vom Nutzer eingegeben Werten.
|
|
Vor dem Ende des Kontextes der Methode wird noch eine weitere Methode \verb|delete_route| aufgerufen.
|
|
|
|
\begin{lstlisting}[language=python,
|
|
frame=single, % Ein Rahmen um den Code
|
|
framexleftmargin=15pt, % Rahmen link von den Zahlen
|
|
style=algoBericht,
|
|
label={lst:deleteroute},
|
|
captionpos=b, % Caption unter den Code setzen
|
|
caption={delete\_route Methode}]
|
|
def delete_route(instance):
|
|
consul_instance = prepare_consul(os.getenv("CONSUL_HOST"), os.getenv("CONSUL_PORT"))
|
|
prefix = str(instance.prefix)
|
|
prefix_encoding = get_prefix_encoding(prefix)
|
|
consul_instance.kv.delete(
|
|
f'v1/route/global/{prefix_encoding}/{prefix.replace("/", "_")}'
|
|
)
|
|
update_active_injectors(instance)
|
|
\end{lstlisting}
|
|
|
|
Hier findet nun das eigentliche Übermitteln der Daten an Consul statt.
|
|
|
|
|
|
\chapter{Injector Komponente}
|
|
|
|
\section{Aufgaben}
|
|
|
|
Der Injector ist der zentrale Baustein des Route Injection Service, der die Möglichkeit bietet, mittels \ac{BGP} Communities, Routen in das Netzwerk zu injizieren.
|
|
Der Injector erfüllt dabei eine Reihe von wesentlichen Aufgaben:
|
|
\vspace{1cm}
|
|
|
|
Zuallererst ist der Injector für die Konvertierung der von der \ac{API} empfangenen Routen in eine für den Router (Bird) verständliche Konfigurationsdatei verantwortlich.
|
|
Diese Konvertierung ist von entscheidender Bedeutung, um die Weiterleitung der Routen an den Router in einem kompatiblen Format sicherzustellen.
|
|
Während die Validierung der Präfixe und Communities von der \ac{API} Komponente übernommen wird, hat der Injector eine eigene Validierung für Routen, welchen über den Emergency-Mode angegeben werden, da hier die \ac{API} Komponente überbrückt wird.
|
|
Bei auftretenden Konflikten oder Unstimmigkeiten kann der Injector angemessene Maßnahmen ergreifen, um die Integrität der anderen Komponenten und schlussendlich des Netzwerks, zu gewährleisten.
|
|
Ein wichtiger Aspekt ist auch die aktive Kommunikation des Injectors mit dem Router (Bird).
|
|
Diese Kommunikation erfolgt, um die generierten Konfigurationsänderungen effektiv zu übertragen und sicherzustellen, dass die injizierten Routen nahtlos in das Routing-Protokoll des Routers integriert werden.
|
|
Schließlich stellt der Injector durch präzises loggen sicher, dass im Falle eines Fehlers, oder im schlimmsten Fall, bei einem Absturz der Komponente, Ereignisse festgehalten werden.
|
|
Zusammenfassend fungiert der Injector als entscheidende Schnittstelle, die die Funktionen der \ac{API} und des Routers miteinander verbindet.
|
|
Mit seiner intelligenten Konvertierung und Verwaltung von Routen durch \ac{BGP} Communities gewährleistet er, dass die gewünschten Routing-Änderungen präzise und effizient im \ac{BGP}-Netzwerk implementiert werden.
|
|
|
|
\newpage
|
|
|
|
\section{Umsetzung}
|
|
|
|
\subsection{Generieren der Config Files für Bird} %TODO RNDMISC-410
|
|
|
|
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.
|
|
|
|
\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}
|
|
|
|
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}
|
|
|
|
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/<injector_id>/heartbeat|.
|
|
Hierbei steht \verb|<injector_id>| 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 <prefix> <communities>
|
|
\item delete-route <prefix>
|
|
\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}
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
\endinput
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|