Dynamische Gateway – deel 2

In het vorige bericht is gesproken over het dynamisch maken van de gateway op een server. Na de setup is er een aanpassing gemaakt om de statische default-route uit te schakelen. Een bijkomend nadelig effect is dat bij een herstart van de server deze onbereikbaar maakt. Waarom? Omdat de FRR daemon start en het netwerk nog niet gereed is en het dus een fout oploopt en faalt. Dus dan raak je aan een console sessie of wellicht DRAC/ILO of misschien wel een fysiek bezoek.

Een oplossing kan in het systemd ecosysteem gevonden worden. Er zijn zelfs 2 (misschien wel meer) manieren mogelijk.

De eerste is een simpele delay timer. Met systemctl edit frr.service kan je aanpassingen maken (zijnde override). Voeg het onderstaande toe onder de eerste 2 (commented) regels:

[Service]
ExecStartPre=/bin/sleep 5

Sla het op en klaar. Maar: wat als het opstarten van netwerk te lang duurt? De timer omhoog is een manier maar niet heel robuust. Het kan ook misschien langer duren dan nodig is, of ook weer falen.

Het kan ook op de volgende manier, in dezelfde edit modus van de systemctl met de FRR service.

[Unit]
BindsTo=systemd-networkd.service
After=systemd-networkd.service

Hiermee koppel je de FRR service aan de systemd-networkd service en word pas gestart als het netwerk gereed is. Eigenlijk is de After voldoende maar de BindsTo is een andere praktische oplossing. Als iets of iemand het netwerk op de server herstart, word FRR meegenomen in de herstart!

Dynamische Gateway

In sommige situaties heb je bij een ISP het zo dat zij geen first-hop-redundacy protocol , zoals HSRP of VRRP, ondersteunen maar wel op een 2-tal edgerouters een gateway voor jou aanbieden binnen hetzelfde subnet. In dit artikel duik ik een klein stukje in het gebruik van BGP en de BGP mogelijkheden op Linux binnen FRRrouting (FRR). Een setup die bij grote cloudproviders ook veel gebruikt kan worden.

Een situatie schets van het netwerk:

In deze situatie zou je een default gateway kunnen zetten naar 1 router. Ja, natuurlijk kan je een 2e zetten maar bij uitval of onderhoud van een router heb je daar last van (de server weet namelijk niet welke route er gekozen moet worden). Een dynamisch protocol als BGP heeft dat nadeel niet.

We gaan even uit van een minimaal geinstalleerde Ubuntu 22.04 LTS server. Deze heeft R1 als default-gateway om van basis connectiviteit te voorzien. We gaan daarna FRR via de cli installeren en configureren. Ook zetten we de BGP-daemon aan:

apt install -y frr frr-pythontools
sed -i "s/^bgpd=no/bgpd=yes/" /etc/frr/daemons

Hierna kunnen we een eenvoudige BGP-setup in de configuratie opnemen op de server. De routers van de ISP leven in dit voorbeeld in ASN 65001 en wij hebben vanuit onze ISP ASN 65015 toegekend gekregen. We staan via een prefix-list alleen de default-route toe.

/etc/frr/frr.conf:

frr defaults traditional
log syslog informational
!
debug bgp events
debug bgp filters
debug bgp fsm
debug bgp keepalives
debug bgp updates
!
router bgp 65015
 bgp router-id 10.99.75.3
 bgp log-neighbor-changes
 !
 neighbor UPSTREAM peer-group
 neighbor UPSTREAM remote-as 65001
 neighbor UPSTREAM soft-reconfiguration inbound
 !
 neighbor 10.99.75.1 peer-group UPSTREAM
 neighbor 10.99.75.1 description r1
 !
 neighbor 10.99.75.2 peer-group UPSTREAM
 neighbor 10.99.75.2 description r2
 !
 address-family ipv4 unicast
  neighbor UPSTREAM prefix-list DEFAULT-ONLY in
!
ip prefix-list DEFAULT-ONLY seq 5 permit 0.0.0.0/0
ip prefix-list DEFAULT-ONLY seq 10 deny any

Nu deze configuratie staat kunnen we de daemon starten:

systemctl restart frr

In ons geval heeft onze ISP de configuratie op hun routers al toegepast en komen de sessies gelijk op. Deze kunnen we bekijken via de meegeleverde vtysh tool die ook een enkel commando accepteert. We kijken naar de summary (of de sessies up zijn) en naar de ontvangen routes:

root@server1:~$ vtysh -c "show ip bgp summ"

IPv4 Unicast Summary (VRF default):
BGP router identifier 10.99.75.3, local AS number 65015 vrf-id 0
BGP table version 2
RIB entries 1, using 184 bytes of memory
Peers 2, using 1446 KiB of memory
Peer groups 1, using 64 bytes of memory
Neighbor        V         AS   MsgRcvd   MsgSent   TblVer  InQ OutQ  Up/Down State/PfxRcd   PfxSnt Desc
10.99.75.1      4      65001       494       471        0    0    0 01:18:00            1 (Policy) r1
10.99.75.2      4      65001       500       471        0    0    0 01:18:08            1 (Policy) r2

Total number of neighbors 2

root@server1:~# vtysh -c "show ip bgp"
BGP table version is 2, local router ID is 10.99.75.1, vrf id 0
Default local pref 100, local AS 65015
Status codes:  s suppressed, d damped, h history, * valid, > best, = multipath,
               i internal, r RIB-failure, S Stale, R Removed
Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self
Origin codes:  i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found

   Network          Next Hop            Metric LocPrf Weight Path
*= 0.0.0.0/0        10.99.75.1                             0 65001 i
*>                  10.99.75.2                             0 65001 i

Het enige wat resteert is het uitschakelen van de statisch ingestelde default-gateway. Deze kan in /etc/netplan/00-installer-config.yaml gecommentarieerd worden

      #routes:
      #- to: default
      #  via: 10.99.75.1

Nu kan je via netplan apply de configuratie toepassen maar het beste is een eenvoudige herstart van de gehele server. Na herstart kan je weer bij de server. Validatie is dan niet nodig maar je ziet:

root@server1:~$ ip route
default nhid 10 proto bgp metric 20
        nexthop via 10.99.75.2 dev ens192 weight 1
        nexthop via 10.99.75.1 dev ens192 weight 1
10.99.75.0/29 dev ens192 proto kernel scope link src 10.99.75.3

Mocht je deze BGP-setup op een bestaande server installeren, vergeet dan niet poort tcp/179 toe te voegen in je firewall voor communicatie met de routers.

Scheduling jobs

Vanuit de Linux wereld bestaat de term ‘cron’. Deze term komt vanuit het oude Grieks en het betekend tijd. Vanuit oudsher was op Linux de mogelijkheid om op een paar verschillende manieren een scheduled job te starten, zijnde een table (vandaar crontab):

  • user cron
  • /etc/crontab
  • /etc/cron.d/<bestand>
  • /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly

Op deze verschillende plekken, met uitzondering van de eerste, eenvoudig qua syntax een job starten

# minute hour day month day-of-week username command/script
1 12 * * 1 j.doe /opt/myscript.py

Dus elke maandag om 1 minuut over 12 uur word onder de naam j.doe het script uitgevoerd. Het enige verschil met de user cron (crontab -e (edit) of crontab -l (list) is dat de username niet opgevoerd hoeft te worden omdat het in die userspace opereert.

Sinds al een lange poos is de Linux wereld over aan het stappen naar systemd (nouja, is al overgestapt). Deze architectuur en manier van werken is in diverse opzichten een flinke verbetering van de oude init.d scripts/services en dus crontab. Nu is dat geen nieuwtje maar het gebeurt gewoon te makkelijk om “even een crontabje” te zetten.

Systemd staat je toe zelf eenvoudig services te schrijven maar ook targets én timers. De timer functie zoom ik iets meer op in, daar ik vind het een veel betere plek is voor scheduled jobs. Hier komen namelijk ook meer voordelen bij, te denken aan logging/auditing, controle van de jobs, error afhandeling en complexe situaties om pas te starten als ‘iets’ beschikbaar is, of nadat een ander proces succesvol is gestart. De oude cron methode is meer als UDP op het netwerk: fire and forget.

Een voorbeeld is een soort disk-check die elk half uur de vrije ruimte opvraagt en boven, zeg, 75% gebruik een email uitstuurt naar de beheerder. Dit kan eenvoudig in een cron maar we maken er nu een service van.

/usr/lib/systemd/system/diskcheck.service

[Unit]
Description=Disk space check

[Service]
ExecStart=/usr/local/bin/syschk -d

De timer binnen systemd. Deze naam moet overeenkomen met de service.

/usr/lib/systemd/system/diskcheck.timer

[Unit]
Description=Disk space check every 30 minutes

[Timer]
OnCalendar=*:0/30
Persistent=true

[Install]
WantedBy=timers.target

En deze timer schakelen we in: systemctl daemon-reload && systemctl enable diskcheck.timer

Om te zien of alles goed werkt kan je eenvoudig een paar commando’s uitvoeren:

  • systemctl status diskcheck.service
  • systemctl status diskcheck.timer

En zo heb je dus een heel simpel iets als nog heel simpel gemaakt binnen de huidige systemd wereld met voordelen van logging en uiteraard als je wil dat de service uit gezet moet worden:

  • systemctl disable diskcheck.timer

Een andere optie is een service te maken die alleen actief is bij opstarten (on-boot). Dit voorbeeld start een script (met 2 command line opties) dat pas start als het netwerk online is én postfix gestart is. Waarom? Als de ‘syschk’ iets afwijkends detecteert moet het een email kunnen uitsturen.

[Unit]
Description=HouseKeeping at boot
After=network-online.target
After=postfix.service

[Service]
Type=simple
ExecStart=/usr/local/bin/syschk -bp

[Install]
WantedBy=multi-user.target

Met name de After regels zijn in dit geval waarmee het gestuurd word.

Als je besluit dat het een background daemon of iets dergelijk moet worden, dan kan dat ook op deze manier maar dan moet je in jouw proces of script wel het altijd aan laten. Denk aan een lowlevel ‘while true’ loop (maar denk aan je cpu cylcles ;).

Het nakijken van statussen en logs is bijvoorbeeld met eerder genoemde systemctl commando. Echter is er ook vanuit het Linux eco systeem een andere optie: journalctl.

  • journalctl -eu diskcheck.service

De opties zijn flink en de gekozen 2 zijn eenvoudig de eerste om mee te beginnen. De ‘-e’ is pager-end, of wel het laatste stukje en de ‘-u’ is de betreffende unit. In dit voorbeeld de eerder gemaakte diskcheck.service.

Uiteraard zijn er veel meer opties met journalctl om in te zoomen op bijv. tijdsvakken en nog meer. Zie ook de ‘manpages’.

Zo makkelijk als het lijkt om een service, met timer of niet, te maken is, een veelal vergeten aspect, natuurlijk wel even denken aan veiligheid. De systemd services kunnen en mogen veel, ‘out-of-the-box’. Dat is voor een lokaal servertje thuis niet direct een probleem maar een webserver of waar gevoelige data verwerkt kan worden is dan een interessant object. Als je daar, op welke manier dan ook, gebruik kan maken van zwakheden dan is dat als kwaadwillende top. Ik ga hier in dit artikel niet dieper op in maar verdiep gerust even in de materie:

https://linux-audit.com/systemd/systemd-features-to-secure-units-and-services

Alles Python

Ik heb al een hele tijd niets geschreven. Na een stapel avonden de vorige webserver (LAMP-stack) vervangen te hebben van CentOS 7 naar AlmaLinux 9 (daar lees je dit nu op) ben ik toch weer begonnen met alles moet in python. Een paar kleine ’tooltjes’, of wel scripts, omzetten en weer actiever zijn in deze scripting taal.

Vanuit mijn vorige werk bij een grote service/cloud provider had ik altijd de neiging om veel bash/shell scriptjes te maken. Of one-liners, omdat het kon. Dat was in de tijd van CentOS4/5, toen Python nog een beetje gek aanvoelde. Nu bij mij huidige werkgever (ook een cloud provider) probeer ik iedereen ook te bewegen handig te worden met basis tools uit de GNU-stal. Maar met een duidelijke kanttekening. Moet je meer dan 6 ofzo regels in bash/shell schrijven, dan ben je eigenlijk al te ver. Bij de derde zoekopdracht op Internet moet je dus ook al bedenken dat je te ver gaat.

Python kan zoveel out-of-the box al. De eigen modules die je kan importeren dekken een heleboel. Denk aan sys, os, re, argparse, math en nog veel meer. Natuurlijk, er zijn gevallen waarbij het niets oplevert. Of toch wel? Ik ben van mening dat het altijd helpt. Het houd je actief bezig met de Python scripting taal.

Een aantal voorbeeld

Ik had bijvoorbeeld een klein scriptje gemaakt dat de load van het linux systeem toonde. Ja met uptime zie je het ook, maar ergens dacht ik; dat kan korter. De werkelijke getallen komen uit een bestand /proc/loadavg en dat is dus eigenlijk kort te maken met een alias of letterlijk een bash-script in bijvoorbeeld /usr/local/bin/

#!/usr/bin/env bash
awk '{OFS=", "; print $1, $2, $3}' /proc/loadavg

Maar ja, dit is eenvoudig in Python te doen

#!/usr/bin/env python3
with open('/proc/loadavg', 'r') as f:
    line = f.read().strip().split()
output = ', '.join(line[:3])
print(f'{output}')

Een ander voorbeeld. Ik had voorheen een eigen RPM repository voor CentOS 7. Hierin zaten, jawel, 2 packages. Een heet telebot, de andere postqmon. De telebot package is plat gezegd een bash-script dat een /etc/telebot.conf verwacht met daarin een Telegram bot token en chat id. Daarna doet het, als alle checks gedaan zijn, een POST met cURL naar de Telegram API. En eerder al, in hetzelfde gedachtegoed, heb ik een Python repository online gezet (Devpi) met daarin het equivalent van de bash telebot.

De tegenhanger in Python is wat uitgebreider. Deze kijkt niet alleen naar /etc/telebot.conf maar ook naar lokale ‘user’ config file locaties zoals ~/.config/. Dit is ook uitbreidbaar. Nu is het stuk configuratie file uitlezen met de re.findall() misschien wat omslachtig. Het kan ook met confparse , die ‘.ini’-like files ook parsed. En alles draait als Class, is abstract en het houd je Python kennis weer up-to-date.

Een one-liner die te gek word maar misschien nog net kan?

for USER in $(grep -vP '/sbin/nologin|/bin/sync|/sbin/shutdown|/sbin/halt' /etc/passwd | cut -d ':' -f1); do sudo passwd -S $USER | grep -oP 'LK' 2>&1 > /dev/null && printf "%s is locked\n" $USER || printf "%s login active\n" $USER; done

Natuurlijk houd niemand je tegen met een text file als dit, een soort Cisco vlan-database, in vlans.txt:

vlan 123
vlan 456
vlan 789
vlan 1234
vlan 2345

En dan omzetten naar Juniper

for v in $(awk '{print $2}' vlans.txt); do printf "set vlans vlan%s vlan-id %s\n" $v $v; done

Output;

set vlans vlan123 vlan-id 123
set vlans vlan456 vlan-id 456
set vlans vlan789 vlan-id 789
set vlans vlan1234 vlan-id 1234
set vlans vlan2345 vlan-id 2345

Maar het kan ook in Python!

with open('vlans.txt', 'r') as f:
    lines = f.readlines()
for line in lines:
    line_parts = line.split()
    vlan_id = linepart[1]
    print(f'set vlans vlan{vlan_id} vlan-id {vlan_id}')

En zo zijn er vast nog meer gevallen.

Conclusie

Alles naar Python. En het liefst naar >3.8 of nieuwer. Niet alles in bash/shell-scripts proberen. Ja, voor one-liners blijft het krachtig, dat kan ik niet ontkennen. Maar hou het dan bij one-liners. Een snelle actie om een paar filteringen te doen op een text file? Awk/grep/sed/cut zijn je vrienden.

Maar moet je het in een for-loop doen, dan kom je vast tot het punt dat je het niet meer knap kan lezen en je fouten in syntax moet oplossen. Dan moet je, als je een editor als VIM opent eigenlijk al afvragen; moet ik het niet gelijk in Python doen?

Dan is het antwoord ja! De ervaring leert, dat als je al langer dan 5 minuten bezig bent je one-liner met een loop te corrigeren en knap te krijgen, dat je al te lang bezig bent: stop dan.

Probeer jezelf te bewegen om alles in Python te doen, hoe klein het misschien ook is. Eenmaal wat meer thuis in de basis, kan je verder naar grotere projecten, wellicht met Classes, wellicht met eigen modules die je importeert?