[bash] Given the IP and netmask, how can I calculate the network address using bash?

Given the IP and netmask, how can I calculate the network address using bash?

 

 

In a bash script I have an IP address like 192.168.1.15 and a netmask like 255.255.0.0. I now want to calculate the start address of this network, that means using the &-operator on both addresses. In the example, the result would be 192.168.0.0. Does someone have something like this ready? I’m looking for an elegant way to deal with ip addresses from bash

 

up vote28down voteaccepted

Use bitwise & (AND) operator:

$ IFS=. read -r i1 i2 i3 i4 <<< "192.168.1.15"
$ IFS=. read -r m1 m2 m3 m4 <<< "255.255.0.0"
$ printf "%d.%d.%d.%dn" "$((i1 & m1))" "$((i2 & m2))" "$((i3 & m3))" "$((i4 & m4))"
192.168.0.0

Example with another IP and mask:

$ IFS=. read -r i1 i2 i3 i4 <<< "10.0.14.97"
$ IFS=. read -r m1 m2 m3 m4 <<< "255.255.255.248"
$ printf "%d.%d.%d.%dn" "$((i1 & m1))" "$((i2 & m2))" "$((i3 & m3))" "$((i4 & m4))"
10.0.14.96

shareimprove this answer

edited Aug 26 ’14 at 6:36

answered Mar 15 ’13 at 10:18

 

https://www.gravatar.com/avatar/21f80e8ae8c0da98241d8a0e07f94e7b?s=32&d=identicon&r=PG

 

kamituel

19.6k24267

 

1

 

Thank you so much! Exactly what I was looking for, beautiful! – Christian Mar 15 ’13 at 16:18

  

 

This is brilliant. Thank you for taking the time to develop it. – jacksonecac Feb 28 at 20:32

add a comment

 

DSL-Modem im Bridging-Modus betreiben

;widows: 2;-webkit-text-stroke-width: 0px;word-spacing:0px“> Von wogri

Wir hören es fast jeden Tag in den Medien: Großflächige DDoS-Angriffe auf strategisch interessante Ziele im Internet. Bei DDoS-Angriffen haben Angreifer in der Regel private Geräte unter ihrer Kontrolle, ohne dass die Besitzer davon wissen.

Ein mögliches Angriffsziel ist hier der alte DSL-Router, dessen Hersteller schon Jahre kein Softwarupdate mehr ausliefert, und der vor Exploits nur so strotzt. Ein Angriff auf dieses Gerät bleibt unbemerkt, da es weiterhin seinen Dienst verrichtet und der Besitzer kein Fehlverhalten feststellen kann. Während einer DDoS-Attacke sendet das Gerät eine Sequenz von IP-Paketen, die dem privaten User wahrscheinlich nicht auffallen.

Dieser Artikel ist der erste Teil einer Serie aus vorerst zwei Artikeln[1]. Der vorliegende größere Teil beschreibt die Änderung der Router-Einstellungen und die Einrichtung eines Linux-Rechners mit Firewall. Der zweite Teil behandelt die Konfiguration von IPv6 mit DHCP und Firewall.

Wie kann ich mein DSL-Modem schützen?

Abgesehen von regelmäßigen Software-Updates gibt es noch eine mögliche Alternative: Das DSL-Modem vom Internet aus nicht ansprechbar zu machen. Das bedeutet, dass man dem Modem keine öffentliche IP-Adresse zuweist. Ohne öffentliche IP-Adresse ist jedoch keine Teilnahme am Internet möglich. Der Trick ist, das Modem in einen Bridging-Modus zu versetzen und die öffentliche IP-Adresse an einen Linux Software-Router zu vergeben.

Ein weiterer Vorteil dieses Modus ist, dass man frei aus den Netzwerkfähigkeiten von Linux schöpfen kann. VLANs, IPv6 Tunnel, Dynamisches DNS, IPSec, Split Horizon DNS – alles wird möglich. Es muss natürlich erwähnt werden, dass man damit eine Linux-Installation als Angriffsziel im Internet exponiert. Im Unterschied zu proprietären DSL-Boxen hat man hier jedoch selbst die Möglichkeit, das Gerät auf dem aktuellen Softwarestand zu halten, wie auch das Gerät mit Firewall-Regeln komplett von außen unzugänglich zu machen.

Bridging-Modus im DSL-Modem

Nicht jedes DSL-Modem unterstützt den Bridging-Modus. Um herauszufinden, ob ein DSL-Modem dazu fähig ist, muss man in der Konfigurationsoberfläche (oder im Manual) nachsehen. Bei meinem Zyxel-Modem war es glücklicherweise sehr einfach. Man kann ganz unkompliziert zwischen Routing und Bridging Mode umschalten.

Router-Konfiguration

WOLFGANG HENNERBICHLER

Router-Konfiguration

Wie man die jeweiligen DSL-Modems in den Bridging-Modus versetzt, ist im Handbuch der jeweiligen Hersteller nachzulesen. Nicht jedes Modem wird diesen Modus unterstützen.

Achtung: Sobald sich das Modem im Bridging-Modus befindet, ist es vorbei mit der Internet-Verbindung. Man möchte daher diesen Schritt daher gut vorbereiten und auf jeden Fall die Zugangsdaten zum DSL-Provider zur Hand haben, falls man doch wieder auf Routing umkonfigurieren möchte.

Was bedeutet Bridging-Modus?

Falls man bisher mit gebridgten Netzwerken nicht so viel zu tun hatte, ist der Bridging-Modus etwas schwer zu begreifen. Was hier passiert ist, dass das Modem etwaige PPPoE-Pakete direkt an die physische DSL-Schnittstelle weiterleitet. Das kann man mit einem WLAN Access Point vergleichen, der LAN-Pakete ins WLAN weiterleitet. Das Modem braucht nicht mal mehr eine IP-Adresse, um als DSL-Bridge zu fungieren. Ich empfehle dennoch, unbedingt eine lokale IP-Adresse auf dem LAN-Interface zu konfigurieren, damit das DSL-Modem nach wie vor über eine Administrations-Schnittstelle konfiguriert werden kann. Diese lokale IP-Adresse wird von der Linux Firewall geschützt und ist von außen nicht zugänglich, außer man befiehlt der Linux-Firewall explizit, Pakete an dieses Interface weiterzuleiten.

An welche IP-Adresse müssen PPPoE-Pakete geschickt werden?

Die genaue Erklärung findet man auf Wikipedia[2], die kurze Antwort hier: Man muss es nicht konfigurieren. PPPoE hat zwei Phasen. Zuerst die Discovery Phase (auch PADI genannt, steht für PPPoE Active Discovery Initiation) in der ein spezieller Ethernet Broadcast (an FF:FF:FF:FF:FF:FF) geschickt wird. Als Antwort erhält der Absender die MAC-Adresse des DSL-Modems. Ab diesem Zeitpunkt weiß der PPPoE-Client, an welche Ziel-MAC er die PPPoE-Pakete schicken soll.

Das Modem im Bridging-Modus nimmt die PPPoE-Pakete in Empfang und leitet sie an das DSL-Interface weiter. Aus diesem Grund braucht das DSL-Modem keine IP-Adresse im LAN, sobald es als Bridge funktioniert. Wie oben erwähnt empfehle ich dennoch, eine Management-IP auf das LAN-Interface des Modems zu konfigurieren.

Linux wird zum DSL-Router

Unterstützt das Modem den Bridging-Modus, braucht man noch einen Computer für die DSL-Einwahl. Ein Rasperry Pi mit Linux ist wahrscheinlich ausreichend dafür. Da der Pi seine Ethernet-Schnittstelle mit dem restlichen USB-Bus teilt, bevorzuge ich in meinem Beispiel eine virtuelle Maschine mit Debian Jessie, um die Netzwerk-Latenz für berufliche Videokonferenzen gering zu halten.

In diesem Tutorial probiere ich nicht nur, das Ding zum Laufen zu bringen, ich probiere auch, moderne Software dafür zu verwenden. Das war endlich ein Grund, mich zumindest etwas näher mit systemd zu beschäftigen und nftables zu verstehen.

systemd

Wie vorher erwähnt, verwende ich als PPPoE-Dialer und Firewall eine dezidierte virtuelle Maschine mit Debian. Um meine Skripte und Konfigurationsdateien etwas leserlicher zu gestalten, habe ich mir vorgenommen, die physischen Schnittstellen gleich vorneweg richtig zu benennen. Systemd hilft hier sehr, und es ist einfacher als jemals zuvor (ich denke hier an udev), physische Schnittstellen richtig zu benennen. Leider hat Debian Jessie noch nicht alle Features, die wir für unser Setup benötigen. Daher holen wir uns Systemd aus den Backports:

echo deb ftp.debian.org/debian jessie-backports main >> /etc/apt/sources.list
apt-get update
apt-get -t jessie-backports install "systemd"

Zuerst ist es notwendig, systemd-networkd zu aktivieren, um von den vielen Vorteilen der Netzwerkkonfiguration mit systemd zu profitieren: systemct enable systemd-networkd.service aktiviert den networkd. Um ein Interface namens lan als solches zu benennen, geht man nach /etc/systemd/network und erzeugt eine Datei namens 01-lan.link. Der Dateiname muss auf .link enden, und das 01 gibt die Reihenfolge in der Bootsequenz bekannt. Die Datei schaut bei mir folgendermaßen aus:

[Match]
MACAddress=52:54:00:c1:f8:1a
[Link]
Name=lan

Die IP-Adressen des Systems habe ich nicht altmodisch in /etc/network/interfaces konfiguriert, sondern auch mit systemd in der Datei /etc/systemd/network/lan.network:

[Match]
Name=lan
 
[Network]
Address=192.168.1.1/24
IPForward=yes

Dann selbiges auch noch mit dem Loopback-Interface in der Datei lo.network:

[Match]
Name=lo
 
[Network]
Address=127.0.0.1/8

PPPoE

PPPoE setzt ein paar Pakete voraus, die eventuell im System noch nicht vorinstalliert sind:

apt-get install pppd pppoeconf

Mit pppoeconf kann man die angesprochene Discovery anstoßen. Bevor wir die Discovery anstoßen, sollten wir aber die PPP-Parameter noch setzen. Zuerst tragen wir die Zugangsdaten zu unserem Provider in /etc/ppp/chap-secrets ein:

wogri@myprovider.com * my-secret-password *

Danach konfigurieren wir den pppd in /etc/ppp/peers/provider. Die Datei namens provider stellt den Default-Provider für pppd dar. Da die meisten User nur einen DSL-Provider haben, ist es praktisch diese Datei zu verwenden, da sonsten immer der Provider in den anschließenden Befehlen separat angegeben werden muss. Hier also die Konfiguration:

# Use Roaring Penguin's PPPoE implementation.
plugin rp-pppoe.so lan
 
# Login settigns.
user "wogri@myprovider.com&quot;
noauth
hide-password
 
# Connection settings.
persist
maxfail 0
holdoff 5
 
# LCP settings.
lcp-echo-interval 10
lcp-echo-failure 3
 
# PPPoE compliant settings.
noaccomp
default-asyncmap
mtu 1492
 
# IP settings.
noipdefault
defaultroute

Man beachte hier die MTU von 1492. Bei Ethernet ist der Default 1500. Das wird im weiteren Verlauf der Konfiguration noch wichtig (Stichwort MSS Clamping).

Mit pon sollte man nun eine Verbindung zum ISP aufbauen können. ip address show ppp0 sollte die verhandelte IPv4-Konfiguration zeigen.

PPP-Verbindung beim Booten aufbauen

Natürlich müssen wir sicherstellen, dass bei einem Neustart die PPP-Verbindung wieder zuverlässig aufgebaut wird. Dafür müssen wir systemd mitteilen, dass wir eine neuen Service haben:

cat <<EOF >/etc/systemd/system/pppoe.service
[Unit]
Description=PPPoE connection
After=network-online.target
 
[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/usr/bin/pon
ExecStop=/usr/bin/poff -a
 
[Install]
WantedBy=default.target
EOF
systemctl daemon-reload
systemctl enable pppoe.service
systemctl start pppoe.service

Routing

Das IPForward=yes in der systemd-networkd-Konfiguration erledigt das Setzen der bekannten ip_forwarding-Parameter, somit sollte ein manuelles Bearbeiten von sysctl.conf nicht notwendig sein.

Die Maschine sollte von nun an mit dem Internet verbunden sein. Was hier aber noch fehlt, ist das Masquerading. Da das interne Netzwerk mit privaten IP-Adressen betrieben wird, ist es notwendig, die private Quell-IP-Adresse der nach außen gerichteten Pakete zu verändern. Unter Linux wird diese Technik Masquerading genannt. Da dieses Tutorial neue Technologien verwendet, werde ich zuerst auf die Masquerading-Konfiguration von nftables eingehen, danach auch kurz iptables aufzeigen.

Firewall

Ziel dieses Tutorials ist es, die Angriffsfläche aufs Heimnetzwerk zu verkleinern. Daher konfigurieren wir eine Firewall, die per Default von außen keine Verbindungen erlaubt. Ich empfehle StrongSwan + IKEv2, sollte jemand das Bedürfnis haben, von außen auf sein Heimnetzwerk zugreifen zu wollen.

nftables

NFTables[3] ist die geplante Ablöse von IPTables. NFTables befindet sich bereits im Linux-Kernel seit Version 3.13. Ein schönes Tutorial über nftables[4] hat Martin Loschwitz im Linux-Magazin geschrieben. Mein Fazit zu nftables ist, dass es bereits ganz gut verwendbar ist, jedoch noch ein paar Features fehlen, bzw. teilweise die Syntax für mich nicht ganz verständlich ist. Fehlende Features für meine Anwendungsfälle sind:

  • mangelnde IPSec-Integration
  • teilweise inkonsistente Syntax der nft-Befehlszeile.
  • Spezial-Features wie MSS Clamping (mehr dazu später) sind noch nicht verfügbar.

Ich gehe davon aus, dass diese Probleme mit der Zeit gelöst werden.

Nun zur Installation: Das Userspace-Tool ist in Debian Jessie nur aus den Backports zu beziehen:

apt-get -t jessie-backports install "nftables"

Nftables kann man wie iptables mit einzelnen Befehlen betreiben oder aber über eine Datei, die atomar in den Kernel geladen (oder geändert) wird. Ich bevorzuge, eine Datei zu pflegen, die atomar in den Kernel geladen wird, ähnlich zum pf-System unter BSD. Eine weitere Eigenheit von nftables NAT ist, dass iptables NAT nicht geladen sein darf. Daher ist es erforderlich, das iptabes NAT-Modul zu entladen:

rmmod iptables_nat

Hier sieht man ein einfaches Beispiel, das nur ein Masquerading mit nftables macht und sinnvolle Chains für die weitere Verwendung angelegt hat:

#!/usr/sbin/nft -f
 
flush ruleset
 
table inet filter {
  chain input {
    type filter hook input priority 0;
    iifname lo accept
    iifname lan accept
    iifname ppp0 jump input_ppp0
    drop
  }
  chain input_ppp0 { # rules applicable to public interface
    ct state {established,related} counter accept
    ct state invalid counter drop
    log
    drop
  }
  chain ouput {
    type filter hook output priority 0;
    accept
  }
  chain forward {
    type filter hook forward priority 0;
    iifname ppp0 counter jump from_internet
    accept
  }
  chain from_internet {
    ct state {established,related} counter accept
    ct state invalid counter drop
    drop
  }
}
 
table ip nat {
  chain prerouting {
    type nat hook prerouting priority 0;
  }
  chain postrouting {
    type nat hook postrouting priority 0;
    oifname ppp0 counter masquerade
  }
}

Mit diesem Regelwerk sollte die einfachste Variante von nft, nämlich ein simples NAT, erledigt sein. Wer ein Beispiel mit mehr Komplexität in seinen Regeln braucht, hier meine Konfiguration, inkl. IPv6:

#!/usr/sbin/nft -f
 
define server_net = 1.2.3.0/28
define my_phone = 192.168.1.100
 
# ipv6
define dovecot_ip6 = 2001:dead:beef:2::143
define server_net_ip6 = 2001:dead:beef::/64
 
flush ruleset
 
table inet filter {
  chain input {
    type filter hook input priority 0;
    iifname lo accept
    iifname lan accept
    iifname servlan accept
    iifname ipsec0 accept
    iifname ppp0 jump input_ppp0
    drop
  }
  chain input_ppp0 { # rules applicable to public interface
    ct state {established,related} counter accept
    ct state invalid counter drop
    ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 10/second counter accept
    ip6 nexthdr icmpv6 icmpv6 type {
      destination-unreachable, packet-too-big, time-exceeded, 
      parameter-problem, nd-router-advert, nd-neighbor-solicit, 
      nd-neighbor-advert } counter accept
    ip protocol icmp icmp type echo-request limit rate 10/second counter accept
    ip6 daddr fe80::/64 udp dport dhcpv6-client counter accept
    ip6 saddr $server_net_ip6 tcp dport {22} counter accept
    # letsencrypt
    ip saddr 0.0.0.0/0 tcp dport {80,443} counter accept
    ip6 saddr ::/0 tcp dport {80,443} counter accept
    # ipsec
    ip protocol esp accept
    ip saddr 0.0.0.0/0 udp dport {500,4500} counter accept
    log
    drop
  }
  chain ouput {
    type filter hook output priority 0;
    accept
  }
  chain forward {
    type filter hook forward priority 0;
    iifname ppp0 counter jump from_internet
  }
  chain from_internet {
    ct state {established,related} counter accept
    ct state invalid counter drop
    ip6 daddr $dovecot_ip6 jump to_dovecot
    log
    drop
  }
  chain to_dovecot {
    ip6 saddr $server_net_ip6 tcp dport {22} counter accept
  }
}
 
table ip nat {
  chain prerouting {
    type nat hook prerouting priority 0;
    iifname ppp0 counter jump dnat_from_internet 
  }
  chain dnat_from_internet {
    udp dport { sip, 16384-16400 } counter dnat $my_phone
  }
  chain postrouting {
    type nat hook postrouting priority 0;
    oifname ppp0 counter masquerade
  }
}

iptables

Wer sich nftables nicht antun möchte, kann das simple NAT mit IPv4 mit iptables folgendermaßen einrichten:

#!/bin/bash                                                                         
                                                                                    
iptables --flush                                                                    
iptables -t nat --flush                                                             
iptables -X                                                                         
                                                                                    
set -x                                                                              
iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE                                
iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 
iptables -A FORWARD -i ppp0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -i ppp0 -m state --state RELATED,ESTABLISHED -j ACCEPT            
iptables -A INPUT -i ppp0 -j LOG                                                    
iptables -A INPUT -i ppp0 -j DROP                                                   
set +x                                                                              

Firewall-Skript automatisch laden

Egal ob man iptables oder nftables verwendet, man muss sicherstellen, dass die Regeln geladen werden, wenn das PPP-Interface hochkommt. Das erledigt man am einfachsten, indem man eine ausführbare Datei nach /etc/ppp/ip-up.d legt. Bei mir sieht der Inhalt folgendermaßen aus:

#!/bin/bash
nft -f /etc/nftables.conf
iptables --flush
iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Ich habe an dieser Stelle nicht überprüft, wann dieses Skript ausgeführt wird – theoretisch könnte es einen kurzen Zeitpunkt geben, wo das PPP-Interface bereits von außen erreichbar, aber die Firewall noch nicht geladen worden ist (oder aufgrund eines Syntax-Fehlers in der Datei die Firewall gar nicht aktiviert wird). Da auf meiner Firewall außer SSH keine Internet-Dienste laufen, mache ich mir darüber keine weiteren Sorgen.

MSS Clamping

Eingangs habe ich erwähnt, dass das PPPoE-Protokoll zum Einsatz kommt. Aus diesem Grund muss man bei der gesetzten MTU besonders aufpassen. PPP setzt die MTU mit dem ISP auf 1492. Ethernet hat eine MTU von 1500. Da vor das PPP-Paket mit 1492 Bytes noch ein 8 Byte Header kommt, kommuniziert der pppd mit dem ISP mit 1500 Bytes.

Wird ein TCP Paket jedoch im Ethernet (das annimmt, 1500 Byte als MTU im gesamten Pfad bis ins Internet zu haben) mit einer Paketgröße von 1500 Bytes versendet, muss unser PPP-Router das Paket fragmentieren.

Hier kommt eine Technik namens Maximum Segment Size Clamping zum Einsatz. Kurzum, mit dem Befehl

iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

wird der Header in TCP-Paketen derart verändert, dass kein TCP-Paket größer als 1492 von innen oder außen gesendet wird. Das kann für einige Provider erforderlich sein. Manche ISPs erlauben jedoch MTUs, die größer als 1500 Bytes sind, dann wird das MSS Clamping nicht benötigt. Für IPv6 ist kein MSS Clamping notwendig, da IPv6 die MTU im gesamten Routing-Pfad ermittelt.

Über den Autor

Wolfgang Hennebichler (Webseite[5]) ist Site Reliability Engineer bei Google.

Linkverweise:


[1] www.pro-linux.de/artikel/1/73/dsl-modem-im-bridging-modus-betreiben.html 
[2] de.wikipedia.org/wiki/PPP_over_Ethernet 
[3] www.pro-linux.de/news/1/21853/netfilter-projekt-veröffentlicht-nftables-04.html 
[4] www.linux-magazin.de/Ausgaben/2014/01/NFtables 
[5] www.wogri.at/

http://www.pro-linux.de/images/NB3/base/print/nb3_logo.png

Mediadaten RSS/Feeds Datenschutz Impressum

© 2017 Pro-Linux

 

Erst du, dann du, dann du, dann du

;widows: 2;-webkit-text-stroke-width: 0px;word-spacing:0px“> Von Mathias Weidner

Vor einiger Zeit kam ein Kollege auf mich zu und fragte, wie er am besten mehrere Backup-Jobs gleichzeitig starten könne. Außerdem sollten ein paar andere Jobs später laufen, wenn die ersten fertig sind. Das Ganze wollte er möglichst schnell und ohne zusätzliche Programme erledigen – also in der Shell.

Mehrere Jobs in der Shell

MATHIAS WEIDNER

Mehrere Jobs in der Shell

Die Abbildung veranschaulicht das Problem: Zum Zeitpunkt t0 starten die ersten Jobs. Sie sind zum Zeitpunkt t1 fertig. Die nächsten Jobs sollen zum Zeitpunkt t2 starten. Eine dritte Staffel sollte frühestens zum Zeitpunkt t3 starten, wenn alle Jobs der zweiten Staffel fertig sind. Außerdem sollen t1 und t2 nicht allzu weit auseinander liegen – es ging ja um Backup-Jobs, die über Nacht laufen und am Morgen fertig sein sollen.

Die erste Idee, die Jobs mit & in den Hintergrund zu schicken, bringt einige Probleme mit sich:

  • Bei Hintergrund-Jobs bekommt ein Shell-Skript nicht mit, wann sie fertig sind. Man muss t1 und t3 daher schätzen und verschenkt Zeit, weil zuviel Reserve bleibt oder die nächste Staffel zu früh startet, während die vorherige noch läuft.
  • Die Ausgaben der einzelnen Jobs vermischen sich. Man kann nicht vorhersagen, wessen Ausgabe an welcher Stelle erscheint.
  • Man bekommt keine Rückgabewerte der Jobs und kann somit im Skript nicht darauf reagieren.

Für diese Probleme gibt es Abhilfen, auch mit den Mitteln der Shell selbst. Doch die meisten benötigen Interprozesskommunikation und temporäre Dateien. Außerdem blähen sie das Skript auf, bis am Ende die eigentlichen Jobs kaum noch zu finden sind. Das möchte man nicht an einen Kollegen weitergeben.

Synchronisation mit Pipes

Es gibt allerdings eine einfache Möglichkeit, Jobs in der Shell gleichzeitig zu starten, bei der die Shell automatisch wartet, bis der letzte der Jobs geendet hat: Prozesse, die über Pipes miteinander verbunden sind, startet die Shell gleichzeitig.

Die Standardausgabe jeder dieser Jobs ist mit der Standardeingabe des nächsten verbunden, und die Standardeingabe mit der Standardausgabe des vorigen Jobs. Auf diese Art reichen die Ausgaben aber nur zum jeweils nächsten Job, und die Shell sieht nur die Ausgabe des jeweils letzten. Die Jobs davor würden zudem blockieren, wenn ihre Ausgabe nicht gelesen wird.

Nun ließe sich die Ausgabe der Jobs mit cat in eine Datei leiten und diese so entsperren:

( cat >> $logfile &; jobx )

Damit würden zwar alle Jobs problemlos laufen. Aber die Ausgabe würde wieder durcheinander in die Logdatei geschrieben. Besser ist es, wenn jeder Job die Ausgabe des vorigen Jobs an den nächsten weiterleiten würde. Das heißt, jeder Job, mit Ausnahme des ersten, müsste etwa so aussehen:

( jobx; cat )

Die runden Klammern schicken die Jobs in eine Subshell, sodass das Semikolon nicht die Pipe auseinanderreißt. Auf diese Weise erscheinen die Ausgaben allerdings in umgekehrter Reihenfolge:

$ echo 1|(echo 2; cat)|(echo 3; cat)
3
2
1

Außerdem kann Job Nummer 2 seine Ausgabe erst vollständig schreiben, wenn Job 3 beendet ist und cat die Ausgabe weiterleitet. Dito für Job 1: Zwar starten alle Jobs zur gleichen Zeit, aber die ersten blockieren, bis die letzten fertig sind. Das lässt sich zwar lösen, wenn die Reihenfolge des cat-Befehls und des Jobs getauscht ist:

$ echo 1|(cat; echo 2)|(cat; echo 3)
1
2
3

Nun stimmt zwar die Reihenfolge. Aber Job 2 startet erst, wenn Job 1 seine Ausgabe schließt. Und Job 3 startet erst, wenn Job 2 seine Standardausgabe schließt.

Hier hilft der Trick, die Ausgabe jedes Jobs in Variablen zwischenzuspeichern und sie erst am Ende auszugeben. Abgesehen davon, dass alle Jobs unabhängig voneinander laufen können, lässt sich die Ausgabe nun auch in die richtige Reihenfolge bringen. Damit es übersichtlicher wird, kapselt eine Shell-Funktion den Aufruf von cat und den Job, der ich den Job als Argument übergebe:

01 in_pipe() {
02   CMD="$*"
03   BEFORE=$(date)
04   LOG=$($CMD)
05   RESULT=$?
06   AFTER=$(date)
07   
08   cat
09   echo $BEFORE
10   echo "$CMD"
11   echo "-----"
12   echo "$LOG"
13   echo "-----"
14   echo "RESULT=$RESULT"
15   echo $AFTER
16   echo "====="
17   return $RESULT
18 }

Diese Funktion zieht alle Register. Zeile 02 speichert alle übergebenen Argumente in einer Variablen, die als Befehl für den Job dient. Die Zeilen 03 und 06 notieren die Zeit vor und nach dem Aufruf des Jobs in zwei weiteren Variablen. Zeile 04 speichert die Ausgaben des Jobs in einer Shell-Variablen, während er läuft, und Zeile 05 hebt den Rückgabewert für später auf. Zeile 08 enthält den Aufruf von cat, der die Ausgabe des vorherigen Jobs durchleitet: Dadurch entspricht die Reihenfolge der Ausgaben der Reihenfolge der Jobs in der Pipe. Ab Zeile 09 kommt die Ausgabe aller gesammelten Werte des aktuellen Jobs, gefolgt von einer Trennzeile. In Zeile 17 schließlich gibt die Funktion den Rückgabewert des Jobs an die aufrufende Shell zurück.

Diese Funktion findet nun wie folgt Einsatz:

01 echo "=====" 
     | in_pipe sleep 3 
     | in_pipe echo Hallo 
     | in_pipe sleep 2
02 date

Die Ausgabe sieht folgendermaßen aus:

=====
Mi 21. Jan 10:01:58 CET 2015
sleep 3
-----
-----
RESULT=0
Mi 21. Jan 10:02:01 CET 2015
=====
Mi 21. Jan 10:01:58 CET 2015
echo Hallo
-----
Hallo
-----
RESULT=0
Mi 21. Jan 10:01:58 CET 2015
=====
Mi 21. Jan 10:01:58 CET 2015
sleep 2
-----
-----
RESULT=0
Mi 21. Jan 10:02:00 CET 2015
=====
Mi 21. Jan 10:02:01 CET 2015

Alle Jobs starten also zur gleichen Zeit, werden aber zu unterschiedlichen Zeiten fertig. Das Skript fährt fort, nachdem der langsamste Job endete.

Probleme und Einschränkungen

Ein Problem gibt es noch, das mit folgendem Aufruf deutlich wird:

01 echo "=====" 
    | in_pipe false 
    | in_pipe sleep 1
02 echo $?

Das Problem wird in der letzten Zeile der Ausgabe offenbar:

=====
Mi 21. Jan 10:09:08 CET 2015
false
-----
-----
RESULT=1
Mi 21. Jan 10:09:08 CET 2015
=====
Mi 21. Jan 10:09:08 CET 2015
sleep 1
-----
-----
RESULT=0
Mi 21. Jan 10:09:09 CET 2015
=====
0

Die Ausgabe zeigt: Obwohl ein Job einen Fehlerwert gemeldet hat, registriert die Shell diesen nicht, weil der letzte Job in der Pipe ohne Fehler endete.

Bei der POSIX– oder Bourne-Shell muss man mit diesem Umstand leben – oder die Ausgabe der Pipe weiter untersuchen. Verwendet man jedoch bash, ksh oder zsh, erscheint folgende Lösungsanweisung im Skript vor der Pipe:

set -o pipefail

Allerdings hat auch diese Lösung Einschränkungen.

  • Pro Job lässt sich nur ein Befehl aufrufen. Immerhin kann man Argumente mitgeben.
  • Da die Ausgabe der Jobs in Variablen zwischenlagert, sollte die Ausgabe sich auf den maximalen Wert für Shell-Variablen beschränken. Wer mehr braucht, muss mit temporären Dateien arbeiten.
  • Keine Ausgabeumleitungen lassen sich an die Funktion durchreichen. Das könnte man allerdings in die Funktion selbst einbauen, wodurch sie für alle Jobs gelten würde.

Wer das im Hinterkopf behält, kann verschiedene Jobs gleichzeitig aufrufen, ohne dass sich aufeinanderfolgende Staffeln von Jobs überlappen oder zwischendurch Zeit zu verschenken.

Autoreninformation

Mathias Weidner studierte Ende der 1980-er Jahre Automatisierungstechnik in Leipzig. Nach verschiedenen Stellen in der Software-Entwicklung arbeitet er als Administrator für Unix/Linux, Netzwerke und Sicherheitsthemen. Seit einiger Zeit schreibt er hin und wieder Bücher dazu, die unter buecher.mamawe.net[1] zu finden sind.

Dieser Artikel ist zuerst erschienen in UpTimes[2], Mitgliederzeitschrift des GUUG e.V., Ausgabe 2016-2. Veröffentlichung mit freundlicher Genehmigung.

Linkverweise:


[1] buecher.mamawe.net 
[2] www.guug.de/uptimes/

http://www.pro-linux.de/images/NB3/base/print/nb3_logo.png

Mediadaten RSS/Feeds Datenschutz Impressum

© 2017 Pro-Linux

 

DSL-Modem im Bridging-Modus betreiben

> Von wogri

Wir hören es fast jeden Tag in den Medien: Großflächige DDoS-Angriffe auf strategisch interessante Ziele im Internet. Bei DDoS-Angriffen haben Angreifer in der Regel private Geräte unter ihrer Kontrolle, ohne dass die Besitzer davon wissen.

Ein mögliches Angriffsziel ist hier der alte DSL-Router, dessen Hersteller schon Jahre kein Softwarupdate mehr ausliefert, und der vor Exploits nur so strotzt. Ein Angriff auf dieses Gerät bleibt unbemerkt, da es weiterhin seinen Dienst verrichtet und der Besitzer kein Fehlverhalten feststellen kann. Während einer DDoS-Attacke sendet das Gerät eine Sequenz von IP-Paketen, die dem privaten User wahrscheinlich nicht auffallen.

Dieser Artikel ist der erste Teil einer Serie aus vorerst zwei Artikeln[1]. Der vorliegende größere Teil beschreibt die Änderung der Router-Einstellungen und die Einrichtung eines Linux-Rechners mit Firewall. Der zweite Teil behandelt die Konfiguration von IPv6 mit DHCP und Firewall.

Wie kann ich mein DSL-Modem schützen?

Abgesehen von regelmäßigen Software-Updates gibt es noch eine mögliche Alternative: Das DSL-Modem vom Internet aus nicht ansprechbar zu machen. Das bedeutet, dass man dem Modem keine öffentliche IP-Adresse zuweist. Ohne öffentliche IP-Adresse ist jedoch keine Teilnahme am Internet möglich. Der Trick ist, das Modem in einen Bridging-Modus zu versetzen und die öffentliche IP-Adresse an einen Linux Software-Router zu vergeben.

Ein weiterer Vorteil dieses Modus ist, dass man frei aus den Netzwerkfähigkeiten von Linux schöpfen kann. VLANs, IPv6 Tunnel, Dynamisches DNS, IPSec, Split Horizon DNS – alles wird möglich. Es muss natürlich erwähnt werden, dass man damit eine Linux-Installation als Angriffsziel im Internet exponiert. Im Unterschied zu proprietären DSL-Boxen hat man hier jedoch selbst die Möglichkeit, das Gerät auf dem aktuellen Softwarestand zu halten, wie auch das Gerät mit Firewall-Regeln komplett von außen unzugänglich zu machen.

Bridging-Modus im DSL-Modem

Nicht jedes DSL-Modem unterstützt den Bridging-Modus. Um herauszufinden, ob ein DSL-Modem dazu fähig ist, muss man in der Konfigurationsoberfläche (oder im Manual) nachsehen. Bei meinem Zyxel-Modem war es glücklicherweise sehr einfach. Man kann ganz unkompliziert zwischen Routing und Bridging Mode umschalten.

Router-Konfiguration

WOLFGANG HENNERBICHLER

Router-Konfiguration

Wie man die jeweiligen DSL-Modems in den Bridging-Modus versetzt, ist im Handbuch der jeweiligen Hersteller nachzulesen. Nicht jedes Modem wird diesen Modus unterstützen.

Achtung: Sobald sich das Modem im Bridging-Modus befindet, ist es vorbei mit der Internet-Verbindung. Man möchte daher diesen Schritt daher gut vorbereiten und auf jeden Fall die Zugangsdaten zum DSL-Provider zur Hand haben, falls man doch wieder auf Routing umkonfigurieren möchte.

Was bedeutet Bridging-Modus?

Falls man bisher mit gebridgten Netzwerken nicht so viel zu tun hatte, ist der Bridging-Modus etwas schwer zu begreifen. Was hier passiert ist, dass das Modem etwaige PPPoE-Pakete direkt an die physische DSL-Schnittstelle weiterleitet. Das kann man mit einem WLAN Access Point vergleichen, der LAN-Pakete ins WLAN weiterleitet. Das Modem braucht nicht mal mehr eine IP-Adresse, um als DSL-Bridge zu fungieren. Ich empfehle dennoch, unbedingt eine lokale IP-Adresse auf dem LAN-Interface zu konfigurieren, damit das DSL-Modem nach wie vor über eine Administrations-Schnittstelle konfiguriert werden kann. Diese lokale IP-Adresse wird von der Linux Firewall geschützt und ist von außen nicht zugänglich, außer man befiehlt der Linux-Firewall explizit, Pakete an dieses Interface weiterzuleiten.

An welche IP-Adresse müssen PPPoE-Pakete geschickt werden?

Die genaue Erklärung findet man auf Wikipedia[2], die kurze Antwort hier: Man muss es nicht konfigurieren. PPPoE hat zwei Phasen. Zuerst die Discovery Phase (auch PADI genannt, steht für PPPoE Active Discovery Initiation) in der ein spezieller Ethernet Broadcast (an FF:FF:FF:FF:FF:FF) geschickt wird. Als Antwort erhält der Absender die MAC-Adresse des DSL-Modems. Ab diesem Zeitpunkt weiß der PPPoE-Client, an welche Ziel-MAC er die PPPoE-Pakete schicken soll.

Das Modem im Bridging-Modus nimmt die PPPoE-Pakete in Empfang und leitet sie an das DSL-Interface weiter. Aus diesem Grund braucht das DSL-Modem keine IP-Adresse im LAN, sobald es als Bridge funktioniert. Wie oben erwähnt empfehle ich dennoch, eine Management-IP auf das LAN-Interface des Modems zu konfigurieren.

Linux wird zum DSL-Router

Unterstützt das Modem den Bridging-Modus, braucht man noch einen Computer für die DSL-Einwahl. Ein Rasperry Pi mit Linux ist wahrscheinlich ausreichend dafür. Da der Pi seine Ethernet-Schnittstelle mit dem restlichen USB-Bus teilt, bevorzuge ich in meinem Beispiel eine virtuelle Maschine mit Debian Jessie, um die Netzwerk-Latenz für berufliche Videokonferenzen gering zu halten.

In diesem Tutorial probiere ich nicht nur, das Ding zum Laufen zu bringen, ich probiere auch, moderne Software dafür zu verwenden. Das war endlich ein Grund, mich zumindest etwas näher mit systemd zu beschäftigen und nftables zu verstehen.

systemd

Wie vorher erwähnt, verwende ich als PPPoE-Dialer und Firewall eine dezidierte virtuelle Maschine mit Debian. Um meine Skripte und Konfigurationsdateien etwas leserlicher zu gestalten, habe ich mir vorgenommen, die physischen Schnittstellen gleich vorneweg richtig zu benennen. Systemd hilft hier sehr, und es ist einfacher als jemals zuvor (ich denke hier an udev), physische Schnittstellen richtig zu benennen. Leider hat Debian Jessie noch nicht alle Features, die wir für unser Setup benötigen. Daher holen wir uns Systemd aus den Backports:

echo deb ftp.debian.org/debian jessie-backports main >> /etc/apt/sources.list
apt-get update
apt-get -t jessie-backports install "systemd"

Zuerst ist es notwendig, systemd-networkd zu aktivieren, um von den vielen Vorteilen der Netzwerkkonfiguration mit systemd zu profitieren: systemct enable systemd-networkd.service aktiviert den networkd. Um ein Interface namens lan als solches zu benennen, geht man nach /etc/systemd/network und erzeugt eine Datei namens 01-lan.link. Der Dateiname muss auf .link enden, und das 01 gibt die Reihenfolge in der Bootsequenz bekannt. Die Datei schaut bei mir folgendermaßen aus:

[Match]
MACAddress=52:54:00:c1:f8:1a
[Link]
Name=lan

Die IP-Adressen des Systems habe ich nicht altmodisch in /etc/network/interfaces konfiguriert, sondern auch mit systemd in der Datei /etc/systemd/network/lan.network:

[Match]
Name=lan
 
[Network]
Address=192.168.1.1/24
IPForward=yes

Dann selbiges auch noch mit dem Loopback-Interface in der Datei lo.network:

[Match]
Name=lo
 
[Network]
Address=127.0.0.1/8

PPPoE

PPPoE setzt ein paar Pakete voraus, die eventuell im System noch nicht vorinstalliert sind:

apt-get install pppd pppoeconf

Mit pppoeconf kann man die angesprochene Discovery anstoßen. Bevor wir die Discovery anstoßen, sollten wir aber die PPP-Parameter noch setzen. Zuerst tragen wir die Zugangsdaten zu unserem Provider in /etc/ppp/chap-secrets ein:

wogri@myprovider.com * my-secret-password *

Danach konfigurieren wir den pppd in /etc/ppp/peers/provider. Die Datei namens provider stellt den Default-Provider für pppd dar. Da die meisten User nur einen DSL-Provider haben, ist es praktisch diese Datei zu verwenden, da sonsten immer der Provider in den anschließenden Befehlen separat angegeben werden muss. Hier also die Konfiguration:

# Use Roaring Penguin's PPPoE implementation.
plugin rp-pppoe.so lan
 
# Login settigns.
user "wogri@myprovider.com&quot;
noauth
hide-password
 
# Connection settings.
persist
maxfail 0
holdoff 5
 
# LCP settings.
lcp-echo-interval 10
lcp-echo-failure 3
 
# PPPoE compliant settings.
noaccomp
default-asyncmap
mtu 1492
 
# IP settings.
noipdefault
defaultroute

Man beachte hier die MTU von 1492. Bei Ethernet ist der Default 1500. Das wird im weiteren Verlauf der Konfiguration noch wichtig (Stichwort MSS Clamping).

Mit pon sollte man nun eine Verbindung zum ISP aufbauen können. ip address show ppp0 sollte die verhandelte IPv4-Konfiguration zeigen.

PPP-Verbindung beim Booten aufbauen

Natürlich müssen wir sicherstellen, dass bei einem Neustart die PPP-Verbindung wieder zuverlässig aufgebaut wird. Dafür müssen wir systemd mitteilen, dass wir eine neuen Service haben:

cat <<EOF >/etc/systemd/system/pppoe.service
[Unit]
Description=PPPoE connection
After=network-online.target
 
[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/usr/bin/pon
ExecStop=/usr/bin/poff -a
 
[Install]
WantedBy=default.target
EOF
systemctl daemon-reload
systemctl enable pppoe.service
systemctl start pppoe.service

Routing

Das IPForward=yes in der systemd-networkd-Konfiguration erledigt das Setzen der bekannten ip_forwarding-Parameter, somit sollte ein manuelles Bearbeiten von sysctl.conf nicht notwendig sein.

Die Maschine sollte von nun an mit dem Internet verbunden sein. Was hier aber noch fehlt, ist das Masquerading. Da das interne Netzwerk mit privaten IP-Adressen betrieben wird, ist es notwendig, die private Quell-IP-Adresse der nach außen gerichteten Pakete zu verändern. Unter Linux wird diese Technik Masquerading genannt. Da dieses Tutorial neue Technologien verwendet, werde ich zuerst auf die Masquerading-Konfiguration von nftables eingehen, danach auch kurz iptables aufzeigen.

Firewall

Ziel dieses Tutorials ist es, die Angriffsfläche aufs Heimnetzwerk zu verkleinern. Daher konfigurieren wir eine Firewall, die per Default von außen keine Verbindungen erlaubt. Ich empfehle StrongSwan + IKEv2, sollte jemand das Bedürfnis haben, von außen auf sein Heimnetzwerk zugreifen zu wollen.

nftables

NFTables[3] ist die geplante Ablöse von IPTables. NFTables befindet sich bereits im Linux-Kernel seit Version 3.13. Ein schönes Tutorial über nftables[4] hat Martin Loschwitz im Linux-Magazin geschrieben. Mein Fazit zu nftables ist, dass es bereits ganz gut verwendbar ist, jedoch noch ein paar Features fehlen, bzw. teilweise die Syntax für mich nicht ganz verständlich ist. Fehlende Features für meine Anwendungsfälle sind:

  • mangelnde IPSec-Integration
  • teilweise inkonsistente Syntax der nft-Befehlszeile.
  • Spezial-Features wie MSS Clamping (mehr dazu später) sind noch nicht verfügbar.

Ich gehe davon aus, dass diese Probleme mit der Zeit gelöst werden.

Nun zur Installation: Das Userspace-Tool ist in Debian Jessie nur aus den Backports zu beziehen:

apt-get -t jessie-backports install "nftables"

Nftables kann man wie iptables mit einzelnen Befehlen betreiben oder aber über eine Datei, die atomar in den Kernel geladen (oder geändert) wird. Ich bevorzuge, eine Datei zu pflegen, die atomar in den Kernel geladen wird, ähnlich zum pf-System unter BSD. Eine weitere Eigenheit von nftables NAT ist, dass iptables NAT nicht geladen sein darf. Daher ist es erforderlich, das iptabes NAT-Modul zu entladen:

rmmod iptables_nat

Hier sieht man ein einfaches Beispiel, das nur ein Masquerading mit nftables macht und sinnvolle Chains für die weitere Verwendung angelegt hat:

#!/usr/sbin/nft -f
 
flush ruleset
 
table inet filter {
  chain input {
    type filter hook input priority 0;
    iifname lo accept
    iifname lan accept
    iifname ppp0 jump input_ppp0
    drop
  }
  chain input_ppp0 { # rules applicable to public interface
    ct state {established,related} counter accept
    ct state invalid counter drop
    log
    drop
  }
  chain ouput {
    type filter hook output priority 0;
    accept
  }
  chain forward {
    type filter hook forward priority 0;
    iifname ppp0 counter jump from_internet
    accept
  }
  chain from_internet {
    ct state {established,related} counter accept
    ct state invalid counter drop
    drop
  }
}
 
table ip nat {
  chain prerouting {
    type nat hook prerouting priority 0;
  }
  chain postrouting {
    type nat hook postrouting priority 0;
    oifname ppp0 counter masquerade
  }
}

Mit diesem Regelwerk sollte die einfachste Variante von nft, nämlich ein simples NAT, erledigt sein. Wer ein Beispiel mit mehr Komplexität in seinen Regeln braucht, hier meine Konfiguration, inkl. IPv6:

#!/usr/sbin/nft -f
 
define server_net = 1.2.3.0/28
define my_phone = 192.168.1.100
 
# ipv6
define dovecot_ip6 = 2001:dead:beef:2::143
define server_net_ip6 = 2001:dead:beef::/64
 
flush ruleset
 
table inet filter {
  chain input {
    type filter hook input priority 0;
    iifname lo accept
    iifname lan accept
    iifname servlan accept
    iifname ipsec0 accept
    iifname ppp0 jump input_ppp0
    drop
  }
  chain input_ppp0 { # rules applicable to public interface
    ct state {established,related} counter accept
    ct state invalid counter drop
    ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 10/second counter accept
    ip6 nexthdr icmpv6 icmpv6 type {
      destination-unreachable, packet-too-big, time-exceeded, 
      parameter-problem, nd-router-advert, nd-neighbor-solicit, 
      nd-neighbor-advert } counter accept
    ip protocol icmp icmp type echo-request limit rate 10/second counter accept
    ip6 daddr fe80::/64 udp dport dhcpv6-client counter accept
    ip6 saddr $server_net_ip6 tcp dport {22} counter accept
    # letsencrypt
    ip saddr 0.0.0.0/0 tcp dport {80,443} counter accept
    ip6 saddr ::/0 tcp dport {80,443} counter accept
    # ipsec
    ip protocol esp accept
    ip saddr 0.0.0.0/0 udp dport {500,4500} counter accept
    log
    drop
  }
  chain ouput {
    type filter hook output priority 0;
    accept
  }
  chain forward {
    type filter hook forward priority 0;
    iifname ppp0 counter jump from_internet
  }
  chain from_internet {
    ct state {established,related} counter accept
    ct state invalid counter drop
    ip6 daddr $dovecot_ip6 jump to_dovecot
    log
    drop
  }
  chain to_dovecot {
    ip6 saddr $server_net_ip6 tcp dport {22} counter accept
  }
}
 
table ip nat {
  chain prerouting {
    type nat hook prerouting priority 0;
    iifname ppp0 counter jump dnat_from_internet 
  }
  chain dnat_from_internet {
    udp dport { sip, 16384-16400 } counter dnat $my_phone
  }
  chain postrouting {
    type nat hook postrouting priority 0;
    oifname ppp0 counter masquerade
  }
}

iptables

Wer sich nftables nicht antun möchte, kann das simple NAT mit IPv4 mit iptables folgendermaßen einrichten:

#!/bin/bash                                                                         
                                                                                    
iptables --flush                                                                    
iptables -t nat --flush                                                             
iptables -X                                                                         
                                                                                    
set -x                                                                              
iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE                                
iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu 
iptables -A FORWARD -i ppp0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -i ppp0 -m state --state RELATED,ESTABLISHED -j ACCEPT            
iptables -A INPUT -i ppp0 -j LOG                                                    
iptables -A INPUT -i ppp0 -j DROP                                                   
set +x                                                                              

Firewall-Skript automatisch laden

Egal ob man iptables oder nftables verwendet, man muss sicherstellen, dass die Regeln geladen werden, wenn das PPP-Interface hochkommt. Das erledigt man am einfachsten, indem man eine ausführbare Datei nach /etc/ppp/ip-up.d legt. Bei mir sieht der Inhalt folgendermaßen aus:

#!/bin/bash
nft -f /etc/nftables.conf
iptables --flush
iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Ich habe an dieser Stelle nicht überprüft, wann dieses Skript ausgeführt wird – theoretisch könnte es einen kurzen Zeitpunkt geben, wo das PPP-Interface bereits von außen erreichbar, aber die Firewall noch nicht geladen worden ist (oder aufgrund eines Syntax-Fehlers in der Datei die Firewall gar nicht aktiviert wird). Da auf meiner Firewall außer SSH keine Internet-Dienste laufen, mache ich mir darüber keine weiteren Sorgen.

MSS Clamping

Eingangs habe ich erwähnt, dass das PPPoE-Protokoll zum Einsatz kommt. Aus diesem Grund muss man bei der gesetzten MTU besonders aufpassen. PPP setzt die MTU mit dem ISP auf 1492. Ethernet hat eine MTU von 1500. Da vor das PPP-Paket mit 1492 Bytes noch ein 8 Byte Header kommt, kommuniziert der pppd mit dem ISP mit 1500 Bytes.

Wird ein TCP Paket jedoch im Ethernet (das annimmt, 1500 Byte als MTU im gesamten Pfad bis ins Internet zu haben) mit einer Paketgröße von 1500 Bytes versendet, muss unser PPP-Router das Paket fragmentieren.

Hier kommt eine Technik namens Maximum Segment Size Clamping zum Einsatz. Kurzum, mit dem Befehl

iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

wird der Header in TCP-Paketen derart verändert, dass kein TCP-Paket größer als 1492 von innen oder außen gesendet wird. Das kann für einige Provider erforderlich sein. Manche ISPs erlauben jedoch MTUs, die größer als 1500 Bytes sind, dann wird das MSS Clamping nicht benötigt. Für IPv6 ist kein MSS Clamping notwendig, da IPv6 die MTU im gesamten Routing-Pfad ermittelt.

Über den Autor

Wolfgang Hennebichler (Webseite[5]) ist Site Reliability Engineer bei Google.

Linkverweise:


[1] www.pro-linux.de/artikel/1/73/dsl-modem-im-bridging-modus-betreiben.html 
[2] de.wikipedia.org/wiki/PPP_over_Ethernet 
[3] www.pro-linux.de/news/1/21853/netfilter-projekt-veröffentlicht-nftables-04.html 
[4] www.linux-magazin.de/Ausgaben/2014/01/NFtables 
[5] www.wogri.at/

http://www.pro-linux.de/images/NB3/base/print/nb3_logo.png

Mediadaten RSS/Feeds Datenschutz Impressum

© 2017 Pro-Linux

 

Image-Dateien virtueller Maschinen flink übers Netzwerk kopieren

> Von Urs Pfister

Unter Linux dürften allgemein die Programme »scp« und »rsync« bekannt sein, um Dateien von einem auf einen anderen Rechner zu übertragen. Im Rahmen unserer Virtualisierungslösung ArchivistaVM sowie der freien Version ArchivistaMini werden die Instanzen mit DRDB jeweils auf zwei Maschinen gespeichert. Die erste Maschine überträgt die Daten der ersten Platte auf die zweite Platte der zweiten Maschine, die zweite Maschine überträgt ihre Daten (erste Platte) auf den dritten Rechner (zweite Platte) und so weiter, bis der letzte Rechner des Clusters die Daten auf die erste Maschine in die zweite Platte überträgt.

Geht es darum, Instanzen innerhalb des Clusters von einem Knoten auf einen anderen Knoten zu übertragen, so können dabei beträchtliche Daten anfallen. Virtuelle Festplatten-Dateien in der Größenordnung mehrer hundert Gbyte sollen dabei möglichst effizient auf den gewünschten Zielrechner übertragen werden. Erschwerend hinzu kommt, dass diese Festplatten-Dateien meist im sogenannten Sparse-Format vorliegen, d.h. zu Beginn wird eine Datei erzeugt, die maximal z.B. 200 Gbyte groß werden kann, allerdings nehmen diese Images nur soviel Platz ein, wie es bereits Daten in der Instanz gibt. So gesehen kann eine Sparse-Datei mit 200 Gbyte angelegt werden, sie benötigt am Anfang aber nur 0 Bytes. Der Unterschied wird ersichtlich, wenn die Dateien mit ls -ls gelistet werden:

       0 -rw-r--r-- 1 root root 214748364800 Nov 18 13:51 vm-230-disk-2.raw
12378804 -rw-r--r-- 1 root root  13128695808 Mar 23  2013 vm-230-disk.qcow2
29440368 -rw-r--r-- 1 root root 128849018880 Nov  3 12:38 vm-230-disk.raw

Die erste Zeile zeigt eine solche Datei. In der Datei sind 0 Bytes belegt, bei der zweiten Angabe wird die maximale Größe der Datei (hier 200 Gbyte) ausgeweisen. Beim zweiten Beispiel handelt es sich um eine Datei, die nicht mit dem Sparse-Format erstellt wurde und bei der dritten Datei sind ca. 29 GB belegt, die Datei kann jedoch bis zu 120 Gbyte Platz einnehmen.

Geht es darum, derartige Dateien von einem Rechner auf einen anderen Rechner zu verschieben, was bei der Migration von Maschine A auf B der Fall ist, so gilt es einige Besonderheiten zu beachten. Erstens eignet sich scp grundsätzlich nicht dafür, solche Dateien zu kopieren. Eine Sparse-Datei mit 0 Bytes und 200 Gbyte Maximalgrösse wird mit scp auf 200 Gbyte »aufgeblasen«. Auch rsync löst den Job nicht wirklich ideal, denn ohne die Option --sparse oder -S wird auf dem Zielrechner ebenfalls eine Datei mit 200 Gbyte erstellt. Vor allem aber lässt sich rsync beim Kopiervorgang viel Zeit. Sofern die Daten über eine 1 Gbit-Netzwerkkarte übertragen werden, können gut und gerne mehrere Stunden vergehen, bis die Daten den Zielrechner erreichen, auch wenn auf beiden Maschinen schnelle SSD-RAID-Verbünde (Durchsatz 800 MB/s) vorhanden sind. Sofern mit 1 Gbit-Netzwerkkarten gerarbeitet wird, lässt sich ja noch halbwegs erahnen, dass die Karte den Flaschenhals darstellt, da pro Sekunde ja maximal ca. 125 Mbyte übertragen werden können.

Bei 10 Gbit-Netzwerkkarten müsste dies anders aussehen, da hier pro Sekunde um die 1 Gbyte übertragen werden können. Dennoch konnte mit rsync, egal welche Dateien übertragen wurden, nicht mehr als ein Durchsatz von 200 MB/s erreicht werden. Die Fragestellung lautete daher, lässt sich dieser Prozess schneller gestalten und wenn ja, wie?

Ein Netzlaufwerk mit Samba oder NFS ist eine Alternative. NFS ist unter Linux viel unkomplizierter und in der Regel auch schneller und wird daher vorgezogen. Das Paket wird unter Debian mit apt-get install nfs-kernel-server installiert. Allfällig ist noch apt-get install rpcbind notwendig. Um nun von einem anderen Knoten mit NFS zugreifen zu können, ist die Datei /etc/exports anzulegen. Ein Eintrag kann wie folgt aussehen:

/var/lib/vz/images 10.0.1.126(ro,no_root_squash,async,no_subtree_check)

Nun kann vom betreffenden Rechner aus das Verzeichnis /var/lib/vz/images eingebunden werden, womit die Daten bequem mit cp -rp quelle ziel kopiert werden können. Die ersten Ergebnisse ließen aufhorchen, die großen Dateien konnten viel schneller kopiert werden. Nur die eingangs erwähnte Sparse-Datei mit 0 Bytes wollte irgendwie noch immer nicht so richtig.

Daher wurde der Versuch gestartet, die Daten direkt über das DRBD-Laufwerk zu lesen. Nun können diese Laufwerke ja nur gelesen werden, wenn der DRBD-Knoten den »primary«-Status hat. Dennoch kann der »secondary«-Knoten mit drbdadm down <Name des Devices> heruntergefahren werden. Dann spricht nichts mehr dagegen, das darunterliegende Device lesend zu öffnen. Angenommen, /dev/drbd1 liegt über der Partion /dev/sdb4, so kann mit mount -r -o noatime /dev/sdb4 /mnt/save der »secondary«-Knoten des DRBD-Verbundes auf dem Zielrechner lesend eingebunden werden, dies freilich natürlich nur, wenn der Knoten mit drbdadm down xy zuvor ausgeklinkt wurde.

Hier muss natürlich darauf hingewiesen werden, dass die DRBD-Methode nur möglich ist, wenn man den Cluster von vornherein mit DRBD konfiguriert hat, da eine nachträgliche Einrichtung von DRBD die Partition neu formatieren würde. Um schnell einmal beliebige Dateien zu kopieren, ist DRBD nicht nutzbar. Eine Anleitung zur Einrichtung von DRBD wurde im Rahmen des Workshops »Virtueller hochverfügbarer Linux-Server[1]« veröffentlicht.

Interessanterweise lassen sich die Sparse-Dateien mit cp nun ohne jegliche Zeitverzögerung kopieren, genau dies gelingt bei NFS mit cp so nicht. Die nachfolgenden Zahlen geben einen Einblick, was mit zwei SSD-Platten, einer 10 Gbit-Karte und einem mITX-Board heute machbar ist, wobei immer auch das Zeitverhalten mit rsync mit angegeben ist.

Datei (15G) + Sparse-Datei (30G belegt/130G maximal)

rsync über 1 Gbit/s-Intel-Karte: 1220 Sekunden
NFS über 10 Gbit/s-Intel-Karte: 195 Sekunden
DRBD über 10 Gbit/s-Intel-Karte: 100 Sekunden

http://www.pro-linux.de/images/NB3/imgdb/o_zeitvergleich-datei-15g-sparse-datei-30g-belegt130g-maximal.jpg

URS PFISTER

Geschwindigkeitsvorteil 6,25 bis 12,2

Datei (60G)

rsync über 1 Gbit/s-Intel-Karte: 520 Sekunden
NFS über 10 Gbit/s-Intel-Karte: 150 Sekunden
DRBD über 10 Gbit/s-Intel-Karte: 150 Sekunden

http://www.pro-linux.de/images/NB3/imgdb/o_zeitvergleich-datei-60g.jpg

URS PFISTER

Geschwindigkeitsvorteil 3,4

Datei (150G)

rsync über 1 Gbit/s-Intel-Karte: 1300 Sekunden
NFS über 10 Gbit/s-Intel-Karte: 390 Sekunden
DRBD über 10 Gbit/s-Intel-Karte: 390 Sekunden

http://www.pro-linux.de/images/NB3/imgdb/o_zeitvergleich-datei-150g.jpg

URS PFISTER

Geschwindigkeitsvorteil 3,33, kein Unterschied NFS/DRBD

Sparse-Datei (150G, 0 Bytes enthaltend)

rsync über 10 Gbit/s-Intel-Karte: 1300 Sekunden
NFS über 10 Gbit/s-Intel-Karte: 195 Sekunden
DRBD über 10 Gbit/s-Intel-Karte: 0 Sekunden

http://www.pro-linux.de/images/NB3/imgdb/o_zeitvergleich-sparse-datei-150g-0-bytes-enthaltend.jpg

URS PFISTER

Geschwindigkeitsvorteil 6,8 bis unlimitiert

Gesamtmessung über 9 Images (8 Instanzen)

 

Image

Größe GB

 

1

27/80

 

2

11/11

 

3

3,9/3,9

 

4

5,1/5,1

 

5

2,2/32

 

6

14/120

 

7

13/13

 

8

29/120

 

9

26/84

 

Werden diese Images zusammengezählt, so entstehen 131,2 GByte Daten (27 + 11 + 3,9 + 5,1 + 2,2 + 14 + 13 + 29 + 26), wobei sämtliche Abbilder maximal 469 GByte (80 + 11 + 3,9 + 5,1 + 32 + 120 + 13 + 120 + 84) einnehmen werden können.

Folgende Ergebnisse konnten hier gemessen werden:

rsync über 1 Gbit/s-Intel-Karte: 4275 Sekunden
NFS über 10 Gbit/s-Intel-Karte: 740 Sekunden
DRBD über 10 Gbit/s-Intel-Karte: 595 Sekunden

http://www.pro-linux.de/images/NB3/imgdb/o_zeitvergleich-summe-1312-gb.jpg

URS PFISTER

Geschwindigkeitsvorteil 5,77 bis 7,18

Aus diesen Zahlen wird eindrücklich ersichtlich, dass NFS, noch mehr DRBD-Laufwerke, auch sehr große Dateien jederzeit verschieben können, in allen Testfällen sind NFS und DRBD rsync um mindestens den Faktor 3,33 überlegen, im Schnitt gar um den Faktor irgendwo zwischen sechs und acht. Wer unter diesen Prämissen noch weiter mit rsync arbeitet, ist letztlich ganz einfach selber schuld.

Hinweis: Dieser Beitrag ist für den Vortrag »ArchivistaVM – Cloud-Virtualsierung auf dem Schreibtisch« entstanden, der anlässlich des linxday.at am 26. November in Dornbirn gehalten wurde. Das komplette Skript zur Cloud-Virtualisierung kann von archivista.ch[2] bezogen werden.

Linkverweise:


[1] www.pro-linux.de/artikel/2/1664/projekt-virtueller-hochverfügbarer-linux-server-teil-11.html 
[2] archivista.ch/cms/language/de/aktuell-blog/vortrag-linuxday-at/

http://www.pro-linux.de/images/NB3/base/print/nb3_logo.png

Mediadaten RSS/Feeds Datenschutz Impressum

© 2017 Pro-Linux

 

Add timestamp to history command output in Linux

Add timestamp to history command output in Linux

My last post was about adding timestamp to terminal in Linux. But isn’t it better to simply add timestamp to history command? This enables you to open your terminal anytime, run history command and find out when you ran which command, all without keeping terminal or putty windows open indefinitely. I guess it’s helpful in some cases. Here’s how to add timestamp to history command output in Linux:

Modify .bashrc file

We need to set HISTTIMEFORMAT environment variable. Add the following line to your .bashrc file.

export HISTTIMEFORMAT="%F-%T "

Add timestamp to history command output in Linux - blackMORE Ops - 1

Save the file and open a new terminal, type in history and voila.

root@kali:~# history | tail -5
69 2016-10-11-21:33:48 vi .bashrc
70 2016-10-11-21:34:13 exit
71 2016-10-11-21:34:49 history | tail -5
72 2016-10-11-21:40:47 clear
root@kali:~#

Pretty neat. You can go further by adding it in the skel or default so that it affects all new accounts that are created. You can use config managers such as Puppet or Spacewalk or Chef to deploy these little snippet so that all machines in the managed network has timestamp in history. Either way, this is a nice handy snippet for any Linux Administrator to have.

 

Let’s Encrypt

> Von Christoph Schmidt

Was ist Let’s Encrypt?

Let’s Encrypt[1] ist eine von der Internet Security Research Group (ISRG) betriebene Zertifizierungsstelle (CA, certificate authority), die sich zum Ziel gesetzt hat, das Verschlüsseln von Daten im Internet zum Standard zu machen. Dazu stellt sie kostenlose und leicht installierbare X.509-Zertifikate zur Verfügung, mit denen Internetdienste wie zum Beispiel Webseiten sich im Internet authentifizieren und ihre Daten verschlüsseln können. Nach einer geschlossenen Testphase trat das Projekt am 3. Dezember 2015 in eine öffentliche Testphase ein, in der jeder ein Zertifikat beantragen kann.

Die ISRG ist eine gemeinnützige Organisation mit Sitz in den USA, deren Hauptsponsoren die Electronic Frontier Foundation (EFF), die Mozilla Foundation, Akamai und Cisco Systems sind.

Wozu dienen Zertifikate?

Die von einer CA ausgestellten Zertifikate haben zwei Funktionen: Erstens identifiziert sich der Internetdienst mit diesem Zertifikat, d.h. der Besucher einer Webseite kann sich sicher sein, sich wirklich auf der Seite zu befinden, welche er besuchen wollte. Zweitens wird der Datenverkehr zwischen der besuchten Webseite und dem Rechner des Besuchers verschlüsselt, sodass unbefugte Dritte, die Zugriff auf den Datenstrom haben, diesen nicht mitlesen können.

Webseiten mit einem solchen Zertifikat erkennt man daran, dass die URL (Uniform Resource Locator oder »Internetadresse«) mit https:// anstelle von http:// beginnt. Browser stellen normalerweise auch ein Vorhängeschloss in der Adresszeile vor die URL. Betreibt man nun selbst einen solchen Internetdienst und hat bisher die Komplikationen und/oder Kosten eines Zertifikats gescheut, gibt es nun einen einfachen und kostenlosen Weg, seinen Server mit einem solchen auszustatten.

Let’s Encrypt stellt Zertifikate sowohl für Domains als auch Sub-Domains aus. Eine Domain setzt sich aus dem eigentlichen Domainnamen (z.B. freiesmagazin) und der TLD (Top-Level-Domain, z.B. de) zusammen: freiesmagazin.de. Sub-Domains enthalten einen weiteren vorangestellten Namensteil, welcher sich traditionell auf einen Rechner bezog, z.B. www.freiesmagazin.de. www war früher dabei der Rechner, auf dem der Webserver lief – zu Zeiten, als jeder Service seine eigene Hardware besaß.

Heute werden Sub-Domains häufig bei dynamischem DNS (Domain Name Service) verwendet. Dieser Service wird meist von Privatpersonen in Anspruch genommen, die über einen gewöhnlichen DSL-Anschluss mit dynamischer IP-Adresse mit dem Internet verbunden sind. Dabei ändert der Internet Service Provider (ISP, z.B. Telekom, Vodafone) in regelmäßigen Abständen die IP-Adresse des Anschlusses, sodass der Server im eigenen Heim nicht immer unter der gleichen IP-Adresse vom Internet aus zu erreichen ist. Um dieses Problem zu umgehen, meldet sich der Server nach jeder Änderung der IP-Adresse beim DNS Service, damit dieser die neue IP-Adresse wieder der eigenen Sub-Domain zuweist. Eine solche Sub-Domain hat die allgemeine Form meine-sub-domain.domain.tld, wobei meine-sub-domain ein frei wählbarer Name ist und domain.tld dem DNS-Anbieter gehört.

Installation von Let’s Encrypt

Eines vorneweg: Dieser Artikel beschäftigt sich nicht mit dem Aufsetzen und Konfigurieren eines Webservers, sondern nur mit der Installation und dem Erneuern der Zertifikate. Die Einrichtung eines Webservers würde den Umfang eines einzelnen Artikels bei weitem übertreffen.

Als Beispiel wird in diesem Artikel ein Apache-Webserver auf Debian 8 (»Jessie«) verwendet. Die Installation und Aktualisierung der Zertifikate ist unabhängig vom verwendeten Webserver, lediglich die Einbindung in den Webserver ist verschieden.

Die Let’s Encrypt-Anwendung kann entweder über das Klonen des Projektes von Github oder durch Download eines ZIP-Archivs von dort installiert werden. Let’s Encrypt selbst empfiehlt die Installation von Github wegen der einfacheren Aktualisierung der Anwendung. Dazu muss zuerst git über die Paketverwaltung installiert werden. Anschließend kann das Projekt geklont werden:

$ git clone github.com/letsencrypt/letsencrypt

Nach dem Klonen des Projektes erscheint ein neues Verzeichnis letsencrypt. Zum Aktualisieren wechselt man in das neue Verzeichnis und führt folgenden Befehl aus:

$ git pull

Erstellen des ersten Zertifikates

Zum Erstellen des ersten Zertifikates führt man im Verzeichnis letsencrypt das folgende Kommando aus:

$ ./letsencrypt-auto --server acme-v01.api.letsencrypt.org/directory auth

Läuft der Webserver hierbei hinter einer Firewall, muss sichergestellt sein, dass die Ports 80 (HTTP) und 443 (HTTPS) offen sind. Weiterhin muss der Webserver auch unter der Domain, für die das Zertifikat ausgestellt werden soll, vom Internet aus über DNS erreichbar sein.

Zunächst überprüft letsencrypt-auto, ob irgendwelche Aktualisierungen verfügbar sind, und installiert diese gegebenenfalls. Danach werden dem Anwender einige Fragen gestellt, die zur Erstellung des Zertifikates notwendig sind:

  • Zuerst muss der zu verwendende Webserver angegeben werden. Falls bereits ein Webserver aufgesetzt ist, sollte dieser verwendet werden, da nur ein Webserver auf den HTTP(S) Port zugreifen kann. Ansonsten bietet letsencrypt-auto auch an, einen eigenen Webserver zu verwenden (standalone).
  • Danach wird nach einer E-Mail-Adresse gefragt, unter der das Zertifikat registriert werden soll. Diese wird von Let’s Encrypt für dringende Benachrichtigungen und zur Wiederherstellung verlorener Zertifikate verwendet.
  • Anschließend müssen die Servicebedingungen (terms of service) akzeptiert werden.
  • Zuletzt wird eine Liste mit den Domainnamen abgefragt. Falls Zertifikate für mehr als eine Domain angefordert werden sollen, müssen die Domainnamen mit Komma oder Leerzeichen getrennt werden.

Sobald das Zertifikat erstellt wurde, legt letsencrypt-auto das Verzeichnis /etc/letsencrypt an und speichert dort die Zertifikate unter /etc/letsencrypt/archive/domain.tld/, wobei domain.tld dem Domainnamen entspricht, für welchen das Zertifikat ausgestellt wurde. Alle zukünftigen Zertifikate werden ebenfalls in diesem Verzeichnis abgelegt und durchnummeriert. Das letzte aktuell gültige Zertifikat wird im Verzeichnis /etc/letsencrypt/live/domain.tld/ verlinkt. Von dort sollte der Webserver auch das Zertifikat lesen, da der Link bei jeder Aktualisierung des Zertifikats entsprechend angepasst wird.

Einbinden der Zertifikate

Nachdem die Zertifikate nun auf dem Server installiert sind, müssen diese auch noch im Webserver eingebunden werden. Dies kann entweder automatisch oder manuell erfolgen. Für die automatische Installation muss beim Erstellen des Zertifikates lediglich letsencrypt-auto run anstelle von letsencrypt-auto auth angegeben werden oder – falls die Zertifikate bereits erstellt wurden – letsencrypt-auto install. Die automatische Installation funktioniert derzeit sowohl mit Apache und – momentan noch als Beta – mit NGINX.

Wer nicht möchte, dass letsencrypt-auto die Konfiguration seines Webservers automatisch verändert, kann die Zertifikate auch manuell einbinden. Hierzu muss zuallererst das SSL-Modul in Apache aktiviert werden:

# a2enmod ssl

Danach muss ein virtueller Host für SSL angelegt werden. Hierfür erstellt man eine neue Datei im Verzeichnis /etc/apache2/sites-available. Der Dateiname ist hierbei irrelevant, es empfiehlt sich aber, den Domainnamen im Dateinamen unterzubringen, zum Beispiel ssl.domain.tld.conf, damit man bei diversen Domains die entsprechende Datei schnell findet. Ein minimaler Inhalt für diese Datei könnte der Folgende sein:

<IfModule mod_ssl.c>
    <VirtualHost _default_:443>
 
        SSLEngine on
 
        SSLCertificateFile      /etc/letsencrypt/live/domain.tld/cert.pem
        SSLCertificateKeyFile   /etc/letsencrypt/live/domain.tld/privkey.pem
        SSLCertificateChainFile /etc/letsencrypt/live/domain.tld/chain.pem
 
    </VirtualHost>
</IfModule>
  • SSLEngine: Diese Anweisung aktiviert das SSL/TLS Protokoll für diesen virtuellen Host.
  • SSLCertificateFile: Dies ist der öffentliche Teil des Zertifikat-Schlüssels.
  • SSLCertificateKeyFile: Dies ist der private Teil des Zertifikats-Schlüssels.
  • SSLCertificateChainFile: Diese Datei enthält alle öffentlichen Zertifikat-Schlüssel der CAs, welche das Zertifikat ausgestellt haben.

Anzumerken bleibt noch, dass die oben gezeigte Datei lediglich eine minimale Konfiguration des virtuellen Hosts darstellt. Abhängig vom Einsatzzweck muss die Konfiguration deshalb noch entsprechend erweitert werden. Dazu ist entweder die Dokumentation der verwendeten Distribution oder die der Apache Foundation[2] zu Rate zu ziehen.

Möchte man jede HTTP-Verbindung auf HTTPS umleiten, kann man dafür einen weiteren virtuellen Host anlegen, welcher auf dem HTTP-Port 80 lauscht und die Anfragen entsprechend umleitet:

<VirtualHost *:80>
        RewriteEngine on
        RewriteRule ^ %{HTTP_HOST}/%{REQUEST_URI} [L,QSA,R=permanent]
</VirtualHost>
  • RewriteEngine: Diese Anweisung erlaubt das Ändern von URLs.
  • RewriteRule: Mit dieser Anweisung werden alle HTTP-Anfragen auf HTTPS umgeleitet. Der Rest der URL bleibt dabei unverändert, zum Beispiel domain.tld/login wird zu domain.tld/login. HTTP_HOST entspricht hierbei der Domain (domain.tld) und REQUEST_URI der angeforderten Seite (login).

Nachdem die virtuellen Hosts eingerichtet sind, müssen diese noch aktiviert werden:

# a2ensite ssl.domain.tld

Dieser Befehl legt einen Link in /etc/apache2/sites-enabled auf die neue Seite in /etc/apache2/sites-available an. Sollten bereits Seiten eingerichtet sein, muss man diese gegebenenfalls mit a2dissite deaktivieren oder mit der Anweisung ServerName in der Konfiguration des virtuellen Hosts (in /etc/apache2/sites-available) auf bestimmte Hosts beschränken, um Konflikte zu vermeiden.

Zuletzt wird der Webserver neu gestartet:

# apache2ctl restart

Aktualisieren der Zertifikate

Die von Let’s Encrypt ausgestellten Zertifikate sind nur drei Monate lang gültig. Um nach Ablauf dieses Zeitraums ein neues Zertifikat zu beantragen, kann man die oben beschriebenen Schritte wiederholen oder aber den ganzen Prozess automatisieren. Hierfür müssen lediglich die Antworten für die von letsencrypt-auto gestellten Fragen bereits beim Aufruf mitgeliefert werden. Dies geschieht entweder über die Kommandozeilen-Parameter oder man legt die Konfigurationsdatei cli.ini im Verzeichnis /etc/letsencrypt entsprechend an:

server = acme-v01.api.letsencrypt.org/directory
email = webmaster@domain.tld
authenticator = webroot
webroot-path = /var/www/html/
domains = domain.tld
agree-tos = True
renew-by-default = True
  • server: Hiermit wird der Server angegeben, von dem das Zertifikat bezogen werden soll, d.h. der Let’s Encrypt Server.
  • email: Dies ist die Kontakt-E-Mail für dringende Benachrichtigungen und die Wiederherstellung verlorener Zertifikate.
  • authenticator/webroot-path: Mithilfe dieser Anweisung teilt man letsencrypt-auto mit, dass man zur Authentifizierung einen bereits aufgesetzten Webserver verwenden möchte. letsencrypt-auto wird dann im Verzeichnis webroot-path ein Unterverzeichnis .well-known anlegen, das eine Datei zur Authentifizierung enthält. Mithilfe dieser Datei wird dann sichergestellt, dass der Antragsteller auch wirklich Zugriff auf die Domain besitzt und nicht ein Zertifikat für eine andere, seinem Zugriff nicht unterliegende, Domain anfordert. Der Wert für webroot-path hängt von der verwendeten Distribution und Version ab, ist aber meistens entweder /var/www oder /var/www/html.
  • domains gibt die Domain an, für welche das Zertifikat ausgestellt werden soll.
  • agree-tos: Hiermit werden die Servicebedingungen bestätigt.
  • renew-by-default: Dies fordert ein neues Zertifikat an anstelle einer neuen Kopie des bereits vorhandenen Zertifikats.

Der automatisierte Ablauf kann nun getestet werden, indem man letsencrypt-auto auth als root ausführt. Dabei sollten keine interaktiven Abfragen mehr erscheinen. Anzumerken bleibt noch, dass Let’s Encrypt nur zehn Anfragen pro Domain und Tag zulässt, man muss sich also mit seinen Versuchen etwas zurückhalten.

Funktioniert die Aktualisierung des Zertifikats ohne weitere Interaktion, kann man zum Beispiel eine ausführbare Datei update-cert in /etc/cron.monthly/ mit folgendem Inhalt erstellen:

#!/bin/sh
 
/path/to/letsencrypt-auto auth
apache2ctl restart

Damit wird jeden Monat automatisch ein neues Zertifikat angefordert. /path/to/ muss natürlich noch mit dem Installationsverzeichnis von letsencrypt-auto ersetzt werden.

Fazit

Wer sich bereits etwas mit Webservern auskennt, hat nun einen einfachen und kostenlosen Weg, seinen Server mit einem Zertifikat auszustatten. Auch wenn das Versprechen von Let’s Encrypt, eine Ein-Klick-Lösung anzubieten, noch nicht ganz erreicht ist, dürfte die Installation auch für Hobby-Administratoren zu machen sein.

Die von Let’s Encrypt ausgestellten Zertifikate werden momentan von allen aktuellen Browsern aus den Häusern Microsoft, Google und Mozilla anerkannt. Apple konnte mangels Apple-Geräten nicht getestet werden.

In diesem Sinne: Let’s Encrypt!

Autoreninformation

Christoph Schmidt ist ein Hobby-Linuxer, welcher unter Verständnislosigkeit seiner Umwelt versucht, möglichst viel seiner privaten IT auf FOSS-Lösungen umzustellen.

Dieser Artikel ist in freiesMagazin 01/2016[3] (ISSN 1867-7991) erschienen. Veröffentlichung mit freundlicher Genehmigung.

Linkverweise:


[1] letsencrypt.org/ 
[2] httpd.apache.org/docs/ 
[3] www.freiesmagazin.de/20160103-januarausgabe-erschienen

http://www.pro-linux.de/images/NB3/base/print/nb3_logo.png

Mediadaten RSS/Feeds Datenschutz Impressum

© 2017 Pro-Linux

 

cut out selected fields of each line of a file

Von Markus Schnalke

Funktionsweise

Ursprünglich hatte cut zwei Modi, die später um einen dritten erweitert wurden. cut schneidet entweder gewünschte Zeichen aus den Zeilen der Eingabe oder gewünschte, durch Trennzeichen definierte, Felder.

Der Zeichenmodus ist optimal geeignet, um Festbreitenformate zu zerteilen. Man kann damit beispielsweise bestimmte Zugriffsrechte aus der Ausgabe von ls -l ausschneiden, in diesem Beispiel die Rechte des Besitzers:

$ ls -l foo
-rw-rw-r-- 1 meillo users 0 May 12 07:32 foo
$ ls -l foo | cut -c 2-4
rw-

Oder die Schreibrechte des Besitzers, der Gruppe und der Welt:

$ ls -l | cut -c 3,6,9
ww-

Mit cut lassen sich aber auch Strings kürzen:

$ long=12345678901234567890
$ echo "$long" | cut -c -10
1234567890

Dieser Befehl gibt die ersten maximal 10 Zeichen von $long aus. (Alternativ kann man hierfür printf "%.10sn" "$long" verwenden.)

Geht es aber nicht um die Darstellung von Zeichen, sondern um ihre Speicherung, dann ist -c nicht unbedingt geeignet. Früher, als US-ASCII noch die omnipräsente Zeichenkodierung war, wurde jedes Zeichen mit genau einem Byte gespeichert. Somit selektierte cut -c gleichermaßen sowohl Ausgabezeichen als auch Bytes. Mit dem Aufkommen von Multibyte-Kodierungen (wie UTF-8) musste man sich jedoch von dieser Annahme lösen. In diesem Zug bekam cut mit POSIX.2-1992 einen Bytemodus (Option -b). Will man also nur die ersten maximal 500 Bytes vor dem Newline-Zeichen stehen haben (und den Rest stillschweigend ignorieren), dann macht man das mit:

$ cut -b -500

Den Rest kann man sich mit cut -b 501- einfangen. Diese Funktion ist insbesondere für POSIX wichtig, da man damit Textdateien mit begrenzter Zeilenlänge[1] erzeugen kann.

Wenn auch der Bytemodus neu eingeführt worden war, so sollte er sich doch nur so verhalten wie der alte Zeichenmodus normalerweise schon implementiert war. Beim Zeichenmodus aber wurde eine neue Implementierungsweise gefordert. Das Problem war folglich nicht, den neuen Bytemodus zu implementieren, sondern den Zeichenmodus neu zu implementieren.

Neben dem Zeichen- und Bytemodus bietet cut noch den Feldmodus, den man mit -f einleitet. Mit ihm ist es möglich, Felder auszuwählen. Das Trennzeichen (per Default der Tab) kann mit -d geändert werden. Es gilt in gleicher Weise für die Eingabe und die Ausgabe.

Der typische Anwendungsfall für cut im Feldmodus ist die Auswahl von Information aus der passwd-Datei. Hier z.B. der Benutzername und seine ID:

$ cut -d: -f1,3 /etc/passwd
root:0
bin:1
daemon:2
mail:8
...

Die einzelnen Argumente für die Optionen können bei cut übrigens sowohl mit Whitespace abgetrennt (wie oben zu sehen) als auch direkt angehängt folgen.

Dieser Feldmodus ist für einfache tabellarische Dateien, wie eben die passwd-Datei, gut geeignet. Er kommt aber schnell an seine Grenzen. Gerade der häufige Fall, dass an Whitespace in Felder geteilt werden soll, wird damit nicht abgedeckt. Der Delimiter kann bei cut nur genau ein Zeichen sein. Es kann demnach nicht sowohl an Leerzeichen als auch an Tabs aufgetrennt werden. Zudem unterteilt cut an jedem Trennzeichen. Zwei aneinander stehende Trennzeichen führen zu einem leeren Feld. Dieses Verhalten widerspricht den Erwartungen, die man an die Verarbeitung einer Datei mit Whitespace-getrennten Feldern hat. Manche Implementierungen von cut, z.B. die von FreeBSD, haben deshalb Erweiterungen, die das gewünschte Verhalten für Whitespace-getrennte Felder bieten. Ansonsten, d.h. wenn man portabel bleiben will, verwendet man awk in diesen Fällen.

awk bietet noch eine weitere Funktion, die cut missen lässt: Das Tauschen der Feld-Reihenfolge in der Ausgabe. Bei cut ist die Reihenfolge der Feldauswahlangabe irrelevant; ein Feld kann selbst mehrfach angegeben werden. Dementsprechend gibt der Aufruf von cut -c 5-8,1,4-6 die Zeichen Nummer 1, 4, 5, 6, 7 und 8 in genau dieser Reihenfolge aus. Die Auswahl entspricht damit der Mengenlehre in der Mathematik: Jedes angegebene Feld wird Teil der Ergebnismenge. Die Felder der Ergebnismenge sind hierbei immer gleich geordnet wie in der Eingabe. Um die Worte der Manpage von Version 8 Unix wiederzugeben: »In data base parlance, it projects a relation.« cut[2] führt demnach die Datenbankoperation Projektion auf Textdateien aus. Die Wikipedia erklärt das folgendermaßen[3]:

»Die Projektion entspricht der Projektionsabbildung aus der Mengenlehre und kann auch Attributbeschränkung genannt werden. Sie extrahiert einzelne Attribute aus der ursprünglichen Attributmenge und ist somit als eine Art Selektion auf Spaltenebene zu verstehen, das heißt, die Projektion blendet Spalten aus.«

Geschichtliches

Cut erblickte 1982 mit dem Release von UNIX System III das Licht der öffentlichen Welt. Wenn man die Quellen von System III durchforstet, findet man cut.c mit dem Zeitstempel 1980-04-11[4]. Das ist die älteste Implementierung des Programms, die ich aufstöbern konnte. Allerdings spricht die SCCS-ID im Quellcode von Version 1.5. Die Vorgeschichte liegt – der Vermutung Doug McIlroys[5] zufolge – in PWB/UNIX, dessen Entwicklungslinie die Grundlage für System III war. In den von PWB 1.0 (1977) verfügbaren Quellen[6] ist cut noch nicht zu finden. Von PWB 2.0 scheinen keine Quellen oder hilfreiche Dokumentation verfügbar zu sein. PWB 3.0 wurde später aus Marketinggründen als System III bezeichnet und ist folglich mit ihm identisch. Eine Nebenlinie zu PWB war CB UNIX, das nur innerhalb der Bell Labs genutzt wurde. Das Handbuch von CB UNIX Edition 2.1 vom November 1979 enthält die früheste Erwähnung von cut, die meine Recherche zutage gefördert hat: eine Manpage für cut[7].

Nun ein Blick auf die BSD-Linie: Dort ist der früheste Fund ein cut.c mit dem Dateimodifikationsdatum 1986-11-07[8] als Teil der Spezialversion 4.3BSD-UWisc[9], die im Januar 1987 veröffentlicht wurde. Die Implementierung unterscheidet sich nur minimal von der in System III. Im bekannteren 4.3BSD-Tahoe (1988) tauchte cut nicht auf. Das darauf folgende 4.3BSD-Reno (1990) enthielt aber wieder ein cut. Dieses cut war ein von Adam S. Moskowitz und Marciano Pitargue neu implementiertes cut, das 1989 in BSD aufgenommen wurde[10]. Seine Manpage[11] erwähnt bereits die erwartete Konformität mit POSIX.2. Nun muss man wissen, dass POSIX.2 erst im September 1992 veröffentlicht wurde, also erst gut zwei Jahre, nachdem Manpage und Programm geschrieben worden waren. Das Programm wurde folglich anhand von Arbeitsversionen des Standards implementiert. Ein Blick in den Code bekräftigt diese Vermutung. In der Funktion zum Parsen der Feldauswahlliste findet sich dieser Kommentar:

»This parser is less restrictive than the Draft 9 POSIX spec. POSIX doesn’t allow lists that aren’t in increasing order or overlapping lists.«

Im Draft 11.2 (1991-09) fordert POSIX diese Flexibilität bereits ein:

»The elements in list can be repeated, can overlap, and can be specified in any order.«

Zudem listet Draft 11.2 alle drei Modi, während in diesem BSD cut nur die zwei alten implementiert sind. Es könnte also sein, dass in Draft 9 der Bytemodus noch nicht vorhanden war. Ohne Zugang zu Draft 9 oder 10 war es leider nicht möglich, diese Vermutung zu prüfen.

Die Versionsnummern und Änderungsdaten der älteren BSD-Implementierungen kann man aus den SCCS-IDs, die vom damaligen Versionskontrollsystem in den Code eingefügt wurden, ablesen. So z.B. bei 4.3BSD-Reno: »5.3 (Berkeley) 6/24/90«.

Das cut der GNU Coreutils enthält folgenden Copyrightvermerk:

Copyright (C) 1997-2015 Free Software Foundation, Inc.
Copyright (C) 1984 David M. Ihnat

Der Code hat also recht alte Ursprünge. Wie aus weiteren Kommentaren zu entnehmen ist, wurde der Programmcode zuerst von David MacKenzie und später von Jim Meyering überarbeitet. Letzterer hat den Code 1992 auch ins Versionskontrollsystem eingestellt. Weshalb die Jahre vor 1997, zumindest ab 1992, nicht im Copyright-Vermerk auftauchen, ist unklar.

Trotz der vielen Jahreszahlen aus den 80er Jahren gehört cut, aus Sicht des ursprünglichen Unix, zu den jüngeren Tools. Wenn cut auch ein Jahrzehnt älter als Linux, der Kernel, ist, so war Unix schon über zehn Jahre alt, als cut das erste Mal auftauchte. Insbesondere gehörte cut noch nicht zu Version 7 Unix, das die Ausgangsbasis aller modernen Unix-Systeme darstellt. Die weit komplexeren Programme sed und awk waren dort aber schon vertreten. Man muss sich also fragen, warum cut überhaupt noch entwickelt wurde, wo es schon zwei Programme gab, die die Funktion von cut abdecken konnten. Ein Argument für cut war sicher seine Kompaktheit und die damit verbundene Geschwindigkeit gegenüber dem damals trägen awk. Diese schlanke Gestalt ist es auch, die der Unix-Philosophie entspricht: Mache eine Aufgabe und die richtig! cut überzeugte. Es wurde in andere Unix-Varianten übernommen, standardisiert und ist heutzutage überall anzutreffen.

Die ursprüngliche Variante (ohne -b) wurde schon 1985 in der System V Interface Definition, einer wichtigen formalen Beschreibung von UNIX System V, spezifiziert und tauchte anschließend in allen relevanten Standards auf. Mit POSIX.2 im Jahre 1992 wurde cut zum ersten Mal in der heutigen Form (mit -b) standardisiert.

Multibyte-Unterstützung

Nun sind der Bytemodus und die damit verbundene Multibyte-Verarbeitung des POSIX-Zeichenmodus bereits seit 1992 standardisiert, wie steht es aber mit deren Umsetzung? Welche Versionen implementieren POSIX korrekt? Die Situation ist dreiteilig: Es gibt historische Implementierungen, die nur -c und -f kennen. Dann gibt es Implementierungen, die -b zwar kennen, es aber lediglich als Alias für -c handhaben. Diese Implementierungen funktionieren mit Single-Byte-Encodings (z.B. US-ASCII, Latin1) korrekt, bei Multibyte-Encodings (z.B. UTF-8) verhält sich ihr -c aber wie -b (und -n wird ignoriert). Schließlich gibt es noch Implementierungen, die -b und -c tatsächlich POSIX-konform implementieren.

Historische Zwei-Modi-Implementierungen sind z.B. die von System III, System V und die aller BSDs bis in die 90er.

Pseudo-Multibyte-Implementierungen bieten GNU und die modernen NetBSDs und OpenBSDs. Man darf sich sicher fragen, ob dort ein Schein von POSIX-Konformität gewahrt wird. Teilweise findet man erst nach genauerer Suche heraus, dass -c und -n nicht wie erwartet funktionieren; teilweise machen es sich die Systeme auch einfach, indem sie auf Singlebyte-Zeichenkodierungen beharren, das aber dafür klar darlegen[12]:

»Since we don’t support multi-byte characters, the -c and -b options are equivalent, and the -n option is meaningless.«

Tatsächlich standardkonforme Implementierungen, die Multibytes korrekt handhaben, bekommt man bei einem modernen FreeBSD und bei den Heirloom Tools. Bei FreeBSD hat Tim Robbins im Sommer 2004 den Zeichenmodus POSIX-konform reimplementiert[13]. Warum die beiden anderen großen BSDs diese Änderung nicht übernommen haben, bleibt offen. Es scheint aber an der im obigen Kommentar formulierten Grundausrichtung zu liegen.

Wie findet man nun als Nutzer heraus, ob beim cut des eigenen Systems Multibytes korrekt unterstützt werden? Zunächst einmal ist entscheidend, ob das System selbst mit einem Multibyte-Encoding arbeitet, denn tut es das nicht, dann entsprechen sich Zeichen und Bytes und die Frage erübrigt sich. Man kann das herausfinden indem man sich das Locale anschaut, aber einfacher ist es, ein typisches Mehrbytezeichen, wie zum Beispiel einen Umlaut, auszugeben und zu schauen ob dieses in einem oder in mehreren Bytes kodiert ist:

$ echo ä  | od -c
0000000 303 244  n
0000003

In diesem Fall sind es zwei Bytes: oktal 303 und 244. (Den Zeilenumbruch fügt echo hinzu.)

Mit dem Programm iconv kann man Text explizit in bestimmte Kodierungen konvertieren. Hier Beispiele, wie die Ausgabe bei Latin1 und wie sie bei UTF-8 aussieht:

$ echo ä  | iconv -t latin1 | od -c
0000000 344  n
0000002
$ echo ä  | iconv -t utf8 | od -c
0000000 303 244  n
0000003

Die Ausgabe auf dem eigenen System (ohne die iconv-Konvertierung) wird recht sicher einer dieser beiden Ausgaben entsprechen.

Nun zum Test der cut-Implementierung. Hat man ein UTF-8-System, dann sollte sich eine POSIX-konforme Implementierung folgendermaßen verhalten:

$ echo ä  | cut -c 1 | od -c
0000000 303 244  n
0000003
 
$ echo ä  | cut -b 1 | od -c
0000000 303  n
0000002
 
$ echo ä  | cut -b 1 -n | od -c
0000000  n
0000001

Bei einer Pseudo-POSIX-Implementierung ist die Ausgabe in allen drei Fällen wie die mittlere: Es wird das erste Byte ausgegeben.

Implementierungen

Nun ein Blick auf den Code. Betrachtet wird eine Auswahl an Implementierungen.

Für einen ersten Eindruck ist der Umfang des Quellcodes hilfreich. Typischerweise steigt dieser über die Jahre an. Diese Beobachtung kann hier in der Tendenz, aber nicht in jedem Fall, bestätigt werden. Die POSIX-konforme Umsetzung des Zeichenmodus erfordert zwangsläufig mehr Code, deshalb sind diese Implementierungen tendenziell umfangreicher.

Verschiedene Implementierungen von cut

SLOC

Zeilen

Bytes

Gehört zu

Dateidatum

Kategorie

116

123

2966

System III

1980-04-11

(hist)

118

125

3038

4.3BSD-UWisc

1986-11-07

(hist)

200

256

5715

4.3BSD-Reno

1990-06-25

(hist)

200

270

6545

NetBSD

1993-03-21

(hist)

218

290

6892

OpenBSD

2008-06-27

(pseudo)

224

296

6920

FreeBSD

1994-05-27

(hist)

232

306

7500

NetBSD

2014-02-03

(pseudo)

340

405

7423

Heirloom

2012-05-20

(POSIX)

382

586

14175

GNU coreutils

1992-11-08

(pseudo)

391

479

10961

FreeBSD

2012-11-24

(POSIX)

588

830

23167

GNU coreutils

2015-05-01

(pseudo)

In der Tabelle mit »(hist)« bezeichnete Implementierungen beziehen sich auf ältere Implementierungen, die nur -c und -f kennen. Pseudo-Implementierungen, die -b zwar kennen, es aber nur als Alias für -c handhaben, sind mit »(pseudo)« gekennzeichnet. POSIX-konforme Implementierungen von cut sind mit »(POSIX)« markiert.

Das Kandidatenfeld teilt sich grob in vier Gruppen:

  1. Die zwei ursprünglichen Implementierungen, die sich nur minimal unterscheiden, mit gut 100 SLOCs
  2. Die fünf BSD-Versionen mit gut 200 SLOCs
  3. Die zwei POSIX-konformen Programme und die alte GNU-Version mit 340-390 SLOCs
  4. Und schließlich die moderne GNU-Variante mit fast 600 SLOCs

Die Abweichung zwischen logischen Codezeilen (SLOC, ermittelt mit SLOCcount) und der Anzahl von Zeilenumbrüchen in der Datei (wc -l) erstreckt sich über eine Spanne von Faktor 1,06 bei den ältesten Vertretern bis zu Faktor 1,5 bei GNU. Den größten Einfluss darauf haben Leerzeilen, reine Kommentarzeilen und die Größe des Lizenzblocks am Dateianfang.

Betrachtet man die Abweichungen zwischen den logischen Codezeilen und der Dateigröße (wc -c), so pendelt das Teilnehmerfeld zwischen 25 und 30 Bytes je Anweisung. Die Heirloom-Implementierung weicht mit nur 21 nach unten ab, die GNU-Implementierungen mit fast 40 nach oben. Bei GNU liegt dies hauptsächlich an deren Programmierstil, mit spezieller Einrückung und langen Bezeichnern. Ob man die Heirloom-Implementierung[14] als besonders kryptisch oder als besonders elegant bezeichnen will, das soll der eigenen Einschätzung des Lesers überlassen bleiben. Vor allem der Vergleich mit einer GNU-Implementierung[15] ist eindrucksvoll.

Die interne Struktur der Programmcodes (in C) ist meist ähnlich. Neben der obligatorischen main-Funktion, die die Kommandozeilenargumente verarbeitet, gibt es im Normalfall eine Funktion, die die Feldauswahl in eine interne Datenstruktur überführt. Desweiteren haben fast alle Implementierungen separate Funktionen für jeden ihrer Modi. Bei den POSIX-konformen Implementierungen wird die Kombination -b -n als weiterer Modus behandelt und damit in einer eigenen Funktion umgesetzt. Nur bei der frühen System III-Implementierung (und seiner 4.3BSD-UWisc-Variante) wird außer den Fehlerausgaben alles in der main-Funktion erledigt.

cut-Implementierungen haben typischerweise zwei limitierende Größen: Die Maximalanzahl unterstützter Felder und die maximale Zeilenlänge. Bei System III sind beide Größen auf 512 begrenzt. 4.3BSD-Reno und die BSDs der 90er Jahre haben ebenfalls fixe Grenzen (_BSD_LINE_MAX bzw. _POSIX2_LINE_MAX). Bei modernen FreeBSDs, NetBSDs, bei allen GNU-Implementierungen und bei Heirloom kann sowohl die Felderanzahl als auch die maximale Zeilenlänge beliebig groß werden; der Speicher dafür wird dynamisch alloziert. OpenBSD ist ein Hybrid aus fixer Maximalzahl an Feldern, aber beliebiger Zeilenlänge. Die begrenzte Felderanzahl scheint jedoch kein Praxisproblem darzustellen, da _POSIX2_LINE_MAX mit mindestens 2048 durchaus groß genug sein sollte.

Beschreibungen

Interessant ist zudem ein Vergleich der Kurzbeschreibungen von cut, wie sie sich in der Titelzeile der Manpages oder manchmal am Anfang der Quellcodedatei finden. Die folgende Liste ist grob zeitlich geordnet und nach Abstammung gruppiert:

CB UNIX:

»cut out selected fields of each line of a file«

System III:

»cut out selected fields of each line of a file«

System III (src):

»cut and paste columns of a table (projection of a relation)«

System V:

»cut out selected fields of each line of a file«

HP-UX:

»cut out (extract) selected fields of each line of a file«

4.3BSD-UWisc (src):

»cut and paste columns of a table (projection of a relation)«

4.3BSD-Reno:

»select portions of each line of a file«

NetBSD:

»select portions of each line of a file«

OpenBSD 4.6:

»select portions of each line of a file«

FreeBSD 1.0:

»select portions of each line of a file«

FreeBSD 10.0:

»cut out selected portions of each line of a file«

SunOS 4.1.3:

»remove selected fields from each line of a file«

SunOS 5.5.1:

»cut out selected fields of each line of a file«

Heirloom Tools:

»cut out selected fields of each line of a file«

Heirloom Tools (src):

»cut out fields of lines of files«

GNU coreutils:

»remove sections from each line of files«

Minix:

»select out columns of a file«

Version 8 Unix:

»rearrange columns of data«

»Unix Reader«:

»rearrange columns of text«

POSIX:

»cut out selected fields of each line of a file«

Die mit »(src)« markierten Beschreibungen sind aus dem jeweiligen Quellcode entnommen. Der POSIX-Eintrag enthält die Beschreibung im Standard. Der »Unix Reader[16]« ist ein rückblickendes Textdokument von Doug McIlroy, das das Auftreten der Tools in der Geschichte des Research Unix zum Thema hat. Eigentlich sollte seine Beschreibung der in Version 8 Unix entsprechen. Die Abweichung könnte ein Übertragungsfehler oder eine nachträgliche Korrektur sein. Alle übrigen Beschreibungen entstammen den Manpages.

Oft ist mit der Zeit die POSIX-Beschreibung übernommen oder an sie angeglichen worden, wie beispielsweise bei FreeBSD[17]. Interessant ist, dass die GNU coreutils seit Anbeginn vom Entfernen von Teilen der Eingabe sprechen, wohingegen die Kommandozeilenangabe klar ein Auswählen darstellt. Die Worte »cut out« sind vielleicht auch zu missverständlich. HP-UX hat sie deshalb präzisiert.

Beim Begriff, was selektiert wird, ist man sich ebenfalls uneins. Es wird von Feldern (POSIX), Abschnitten bzw. Teilen (BSD) oder Spalten (Research Unix) geredet. Die seltsame Beschreibung bei Version 8 Unix (»rearrange columns of data«) ist dadurch zu erklären, dass sie aus der gemeinsamen Manpage für cut und paste stammt. In der Kombination der beiden Werkzeuge können nämlich Spalten umgeordnet werden.

Autoreninformation

Markus Schnalke interessiert sich für die Hintergründe von Unix und seinen Werkzeugen. Für die Erarbeitung dieses Textes wurde er regelrecht zum Historiker.

Dieser Artikel ist in freiesMagazin 07/2015[18] (ISSN 1867-7991) erschienen. Veröffentlichung mit freundlicher Genehmigung.

Linkverweise:


[1] pubs.opengroup.org/onlinepubs/9699919799/utilities/cut.html#tag_20_28_17 
[2] man.cat-v.org/unix_8th/1/cut 
[3] de.wikipedia.org/wiki/Projektion_%28Informatik%29#Projektion 
[4] minnie.tuhs.org/cgi-bin/utree.pl?file=SysIII/usr/src/cmd 
[5] minnie.tuhs.org/pipermail/tuhs/2015-May/004083.html 
[6] minnie.tuhs.org/Archive/PDP-11/Distributions/usdl/ 
[7] ftp://sunsite.icm.edu.pl/pub/unix/UnixArchive/PDP-11/Distributions/other/CB_Unix/cbunix_man1_02.pdf 
[8] minnie.tuhs.org/cgi-bin/utree.pl?file=4.3BSD-UWisc/src/usr.bin/cut 
[9] gunkies.org/wiki/4.3_BSD_NFS_Wisconsin_Unix 
[10] minnie.tuhs.org/cgi-bin/utree.pl?file=4.3BSD-Reno/src/usr.bin/cut 
[11] minnie.tuhs.org/cgi-bin/utree.pl?file=4.3BSD-Reno/src/usr.bin/cut/cut.1 
[12] cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/cut/cut.c?rev=1.18&content-type=text/x-cvsweb-markup 
[13] svnweb.freebsd.org/base?view=revision&revision=131194 
[14] heirloom.cvs.sourceforge.net/viewvc/heirloom/heirloom/cut/cut.c?revision=1.6&view=markup 
[15] git.savannah.gnu.org/gitweb/?p=coreutils.git;a=blob;f=src/cut.c;hb=e981643 
[16] doc.cat-v.org/unix/unix-reader/contents.pdf 
[17] svnweb.freebsd.org/base?view=revision&revision=167101 
[18] www.freiesmagazin.de/20150705-juliausgabe-erschienen

 

echo(1) and printf(1)

Bourne | Ash |  #!  | find | ARG_MAX | Shells | whatshell | portability | permissions | UUOC | ancient | – | ../Various | HOME
$@ | echo/printf | set -e | test | tty defs | tty chars | $() vs ) | IFS | using siginfo | nanosleep | line charset | locale


echo(1) and printf(1)

Behaviour of the „echo“ command or the shell built-in, respectively.
Availability of „printf“, and implementation variations (especially handling of escape sequences).

2011-02-27 (see recent changes)


1.) Variations in echo implementations

History

About the history, from Gunnar Ritter in <3B98D626.HL11B46E@bigfoot.de> (rough translation by me), motivating this page:

„Research Unix -> 32v -> BSD had „-n“,
PWB/Unix -> System III -> System V [had] the escape sequences
and then it became a jumble.“

6th edition echo research unix didn’t know any features.
7th edition echo implemented -n
PWB/Unix1.0 echo (derived from 6th edition) implemented \n, \c, \\, and \0xx
System III (and SVR1) echo knew \b, \c, \f, \n, \r, \t, \\, and \0xx

Nowadays, echo(1) is only portable if you omit flags and escape sequences.
Use printf(1) instead, if you need more than plain text.
printf was introduced with the Ninth Edition System (reference in SUSv3).
It was added to more widely distributed Unix flavours with 4.3BSD-Reno and with SVR4.
Meanwhile printf is required by POSIX (SUSv1).

Portability

Traditional portability of echo is one issue. But what does POSIX say about options and backslash sequences?

POSIX doesn’t support plain options:
SUSv2 states that „Implementations will not support any options.“
Since SUSv3 this reads „Implementations shall not support any options.“

But the BSD-like behaviour of suppressing newline with „-n“ is not always forbidden:
The behaviour with an operand containing „-n“ and also the behaviour about backslash sequences is implementation defined.
This gets the various traditional implementations into the boat.

But if the XSI option (X/Open System Interfaces) is supported on the system, then echo is well defined and intended to behave
as specified by the System V Interface Definition, version 3: „-n“ is not recognized and backslash sequences are defined.

More background about this distinction is explained by Don Cragun on the austin group mailing list:

„The specification of \0xxx in echo with the XSI option is intended to behave as UNIX System V behaved as specified by the System V Interface Definition, version 3 (SVID3).
(SVID3 was one of the base documents of the original POSIX.2 standard, but since BSD systems and UNIX System V systems had different behavior for echo,
the original POSIX.2 standard left the behavior unspecified (allowing the then current implementations to continue to behave as they had).
The X/Open Portability Guide, Issue 3 continued to require the behavior as specified in SVID3.
When the original POSIX.2 and XPG3 were merged in a later revision of the standards, we got the XSI option (which required the System V behavior);
but when XSI option support is not claimed the old BSD or System V behavior is allowed.“

If you still have to supress newline with echo for some reason (with the same script on different systems),
a possible workaround is the following code ( ${1+"$@"} instead of "$@" addresses ancient pre-SVR3 shells):

    if [ "X`echo -n`" = "X-n" ]; then
      echo_n() { echo ${1+"$@"}"\c"; }
    else
      echo_n() { echo -n ${1+"$@"}; }
    fi

A closer look at various implementations

(\a is a representative for the other escape sequences)

  • System specific echo implementations, and availability of printf:
    Operating System Bourne sh Korn sh (88) echo(1) printf(1)
      -n  -e  \c  \a  \xxx -n  -e  \c  \a  \xxx -n  -e  \c  \a  \xxx available
    Version 7 (no built-in) N/A x          
    System III (no built-in) N/A     x   x  
    SINIX V5.20 x x -e -e -e (N/A per default) x x -e -e -e  
    SunOS 4 ucb [s4bsd] x         (N/A per default) x          
    SunOS 5 ucb [s5bsd] x         x         x         x
    SunOS 4 sysv [s4sv]     x   x (N/A per default)     x   x  
    SunOS 5 sysv     x   x     x x x     x x x x
    EP/IX 2.2.1AA     x   x     x x x     x   x x
    Unicos 9.0.2.2     N/A         x x x     x x x x
    HP-UX 8.07,9.03     x   x     x x x     x   x x
    HP-UX 10/11.x [hpux]     x   x     x x x     x x x x
    AIX 4.3     x   x     x x x     x x x x
    OSF1/V4, V5 x   x   x     x x x     x x x x
    MUNIX 3.2 (SVR3) x   x   x (N/A per default) x   x   x  
    AIX 3.2 x   x   x x   x x x x   x   x x
    IRIX 5.3, 6.5 x   x   x x   x x x x   x   x x
    OpenServer 506 x   x   x x   x x x x   x x x x
    UnixWare 7.1.4 x   x   x x   x x x x   x   x x
             
      Almquist sh pdksh echo(1) printf(1)
      -n  -e  \c  \a  \xxx -n  -e  \c  \a  \xxx -n  -e  \c  \a  \xxx  
    FreeBSD 2.1 x x -e -e -e 5.2.14 see below x         x
    FreeBSD 2.2, 4.3, 7.1 x x -e -e -e 5.2.14 see below x   x     x
    NetBSD 1.5.1, 5.1 x x -e -e -e 5.2.14 see below x         x
    BSD/OS 4.1 x x -e -e -e 5.2.12 see below x         x
    OpenBSD 2 [pdksh] 5.2.14 see below x         x
             
    Minix 3.1.0 [minix3] x x -e   -e x x -e   -e x
          echo(1) printf(1)
          -n  -e  \c  \a  \xxx  
    POSIX XSI (SysV-like)         x x x x
    POSIX     ?   ? ? ? x
    GNU 2.0.15     x x -e -e -e x
    Busybox 1.01 [busybox]     x x -e -e -e x
  • Shell specific echo implementations:
    Shell Built-ins -n  -e   \c   \a  \xxx printf built-in
    original ash x x x   x  
    debian ash before 0.3.5-7 x x x   x  
    debian ash/dash since 0.3.5-7 x   x x x since ash-0.3.8-1
    ksh86 EP/IX2.2.1AA     x   x  
    ksh88 [see above]  
    ksh93-d .. -q x   x x x x
    ksh93-r .. x x x x x x
    pdksh(5.2.14) x x x x x  
    posh-0.5.4 x   x x x  
    mksh-R28/R39/R52 x x x x x  
    bash-1.14.6/2.x/3.x/4.0 called as bash or sh x x -e -e -e since 2.02
    zsh-3.0.8/4.3.2     called as zsh x x x x x since 4.1.0
    zsh-3.0.8/4.3.2     called as sh x x -e -e -e since 4.1.0
        printf built-in
    NetBSD sh (almquist) [see above] added 11/’02, NetBSD 2.0
    FreeBSD sh (almquist) [see above] removed 11/’01, FreeBSD 5.0
    OpenBSD sh (pdksh) [see above] no

Footnotes for the first table

   
„-e“ feature enabled when using this flag
„?“ implementation defined
[hp-ux] On HP-UX 10, the POSIX shell (/bin/sh) behaves like the Korn shell
[s4bsd] SunOS 4 with /usr/bin preceding /usr/5bin in PATH.
The echo builtin mimics the according external command.
[s5bsd] SunOS 5 enables BSD compatibility, if /usr/ucb precedes /usr/bin in PATH.
On x86, this is also enabled, if SYSV3 is set in the environment
[s4sv] SunOS 4 with /usr/5bin preceding /usr/bin in PATH.
The SysV version knows the escape sequences „\b \c \f \n \r \t \v \xxx“, but not „\a“ (ASCII-BEL).
[minix3] printf(1) has not been documented yet at the time of this writing (06/2010, v3.1.6).
It was added in jan ’87 and released with v2.0.3. It’s located in /usr/bin/.
[busybox]: busybox added printf(1) at about v0.27, may 1995.

2.) printf: variations in the handling of escape seqences

The following table lists tests results.
Keep in mind that some examples use unportable input to illustrate variations.

commandline bash-2.05b bash-4.0 ash-0.4.0 dash-
0.5.5.1
ksh93-k ksh93-t GNU core-
utils 5.97
OpenServer
5.0.6
OSF/1
V4.0B
EP/IX 2.2.1A SunOS 5.9
printf ‚\a‘           | od -b -A n|sed 2d   007   007   007   007   007   007   007   007   007   007   007
printf ‚\b‘           | od -b -A n|sed 2d   010   010   010   010   010   010   010   010   010   010   010
printf ‚\t‘            | od -b -A n|sed 2d   011   011   011   011   011   011   011   011   011   011   011
printf ‚\n‘           | od -b -A n|sed 2d   012   012   012   012   012   012   012   012   012   012   012
printf ‚\v‘           | od -b -A n|sed 2d   013   013   013   013   013   013   013   013   013   013   013
printf ‚\f‘           | od -b -A n|sed 2d   014   014   014   014   014   014   014   014   014   014   014
printf ‚\r‘           | od -b -A n|sed 2d   015   015   015   015   015   015   015   015   015   015   015
printf ‚.\c.‘         | od -b -A n|sed 2d   056 134 143 056   056 134 143 056   056   056 134 143 056   056 143 056   056 156   056 134 143 056   134 143   056 134 143   056 134 143 056   056 134 143 056
printf ‚%b‘ ‚.\c.‘ | od -b -A n|sed 2d   056   056   056   056   056   056   056   056   056   056   056
printf ‚\d‘           | od -b -A n|sed 2d   134 144   134 144   134 144   134 144   144   144   134 144   134 144   134   134 144   134 144
printf ‚\g‘           | od -b -A n|sed 2d   134 147   134 147   134 147   134 147   147   147   134 147   134 147   134   134 147   134 147
printf ‚\h‘           | od -b -A n|sed 2d   134 150   134 150   134 150   134 150   150   150   134 150   134 150   134   134 150   134 150
printf ‚\i‘            | od -b -A n|sed 2d   134 151   134 151   134 151   134 151   151   151   134 151   134 151   134   134 151   134 151
printf ‚\j‘            | od -b -A n|sed 2d   134 152   134 152   134 152   134 152   152   152   134 152   134 152   134   134 152   134 152
printf ‚\k‘           | od -b -A n|sed 2d   134 153   134 153   134 153   134 153   153   153   134 153   134 153   134   134 153   134 153
printf ‚\l‘            | od -b -A n|sed 2d   134 154   134 154   134 154   134 154   154   154   134 154   134 154   134   134 154   134 154
printf ‚\m‘          | od -b -A n|sed 2d   134 155   134 155   134 155   134 155   155   155   134 155   134 155   134   134 155   134 155
printf ‚\o‘           | od -b -A n|sed 2d   134 157   134 157   134 157   134 157   157   157   134 157   134 157   134   134 157   134 157
printf ‚\p‘           | od -b -A n|sed 2d   134 160   134 160   134 160   134 160   160   160   134 160   134 160   134   134 160   134 160
printf ‚\q‘           | od -b -A n|sed 2d   134 161   134 161   134 161   134 161   161   161   134 161   134 161   134   134 161   134 161
printf ‚\s‘           | od -b -A n|sed 2d   134 163   134 163   134 163   134 163   040   163   134 163   134 163   134   134 163   134 163
printf ‚\u‘           | od -b -A n|sed 2d   134 165   134 165   N/A   134 165   165   000   134 165   134 165   134   134 165   134 165
printf ‚\w‘           | od -b -A n|sed 2d   134 167   134 167   134 167   134 167   167   167   134 167   134 167   134   134 167   134 167
printf ‚\y‘           | od -b -A n|sed 2d   134 171   134 171   134 171   134 171   171   171   134 171   134 171   134   134 171   134 171
printf ‚\z‘           | od -b -A n|sed 2d   134 172   134 172   134 172   134 172   172   172   134 172   134 172   134   134 172   134 172
printf ‚\e‘           | od -b -A n|sed 2d   033   033   134 145   134 145   145   033   033   033   033   033   033
printf ‚\033‘       | od -b -A n|sed 2d   033   033   033   033   033   033   033   033   033   033   033
printf ‚\33‘         | od -b -A n|sed 2d   033   033   033   033   033   033   033   033   033   033   033
printf ‚%b‘ ‚\33‘  | od -b -A n|sed 2d   033   033   033   033   134 063 063   134 063 063   033   033   N/A   033   033
printf ‚%b‘ ‚\033‘     | od -b -A n|sed 2d   033   033   033   033   033   033   033   033   033   033   033
printf ‚%b‘ ‚\0033‘   | od -b -A n|sed 2d   033   033   033   033   033   033   033   033   033   033   033
printf ‚\0033‘           | od -b -A n|sed 2d   033   003 063   003 063   003 063   003 063   003 063   003 063   003 063   003 063   003 063   033
printf ‚\0033‘           | od -b -A n|sed 2d   033   003 063   003 063   003 063   003 063   003 063   003 063   003 063   003 063   003 063   033
commandline bash-2.05b bash-4.0 ash-0.4.0 dash-
0.5.5.1
ksh93-k ksh93-t GNU core-
utils 5.97
OpenServer
5.0.6
OSF/1
V4.0B
EP/IX 2.2.1A SunOS 5.9
printf ‚%d\n‘ ‚“a‘   97   97   97   97   97   97   97   97   97   2146938837   97

<http://www.in-ulm.de/~mascheck/various/echo+printf/>

Sven Mascheck

Why is printf better than echo?

 

I have heard that printf is better than echo. I can recall only one instance from my experience where I had to use printf because echo didn’t work for feeding some text into some program on RHEL 5.8 but printf did. But apparently, there are other differences, and I would like to inquire what they are as well as if there are specific cases when to use one vs the other.

shareimprove this question
 

5 Answers

up vote506down voteaccepted

Basically, it’s a portability (and reliability) issue.

Initially, echo didn’t accept any option and didn’t expand anything. All it was doing was outputting its arguments separated by a space character and terminated by a newline character.

Now, someone thought it would be nice if we could do things like echo "\n\t" to output newline or tab characters, or have an option not to output the trailing newline character.

They then thought harder but instead of adding that functionality to the shell (like perl where inside double quotes, \t actually means a tab character), they added it to echo.

David Korn realized the mistake and introduced a new form of shell quotes: $'...' which was later copied by bash and zsh but it was far too late by that time.

Now when a standard Unix echo receives an argument which contains the two characters \ and t, instead of outputting them, it outputs a tab character. And as soon as it sees \c in an argument, it stops outputting (so the trailing newline is not output either).

Other shells/Unix vendors/versions chose to do it differently: they added a -e option to expand escape sequences, and a -n option to not output the trailing newline. Some have a -E to disable escape sequences, some have -n but not -e, the list of escape sequences supported by one echo implementation is not necessarily the same as supported by another.

Sven Mascheck has a nice page that shows the extent of the problem.

On those echo implementations that support options, there’s generally no support of a -- to mark the end of options (zsh and possibly others support - for that though), so for instance, it’s difficult to output "-n" with echo in many shells.

On some shells like bash1 or ksh932 or yash ($ECHO_STYLE variable), the behavior even depends on how the shell was compiled or the environment (GNU echo’s behaviour will also change if $POSIXLY_CORRECT is in the environment). So two bash echos, even from the same version of bash are not guaranteed to behave the same.

POSIX says: if the first argument is -n or any argument contains backslashes, then the behavior is unspecified. bash echo in that regard is not POSIX in that for instance echo -e is not outputting -e<newline> as POSIX requires. The Unix specification is stricter, it prohibits -n and requires expansion of some escape sequences including the \c one to stop outputting.

Those specifications don’t really come to the rescue here given that many implementations are not compliant.

All in all, you don’t know what echo "$var" will output unless you can make sure that $var doesn’t contain backslash characters and doesn’t start with -. The POSIX specification actually does tell us to use printf instead in that case.

So what that means is that you can’t use echo to display uncontrolled data. In other words, if you’re writing a script and it is taking external input (from the user as arguments, or file names from the file system…), you can’t use echo to display it.

This is OK:

echo >&2 Invalid file.

This is not:

echo >&2 "Invalid file: $file"

(Though it will work OK with some (non Unix) echo implementations like bash’s when the xpg_echooption has not been enabled in one way or another like at compilation time or via the environment).

printf, on the other hand is more reliable, at least when it’s limited to the basic usage of echo.

printf '%s\n' "$var"

Will output the content of $var followed by a newline character regardless of what character it may contain.

printf '%s' "$var"

Will output it without the trailing newline character.

Now, there also are differences between printf implementations. There’s a core of features that is specified by POSIX, but then there are a lot of extensions. For instance, some support a %q to quote the arguments but how it’s done varies from shell to shell, some support \uxxxx for unicode characters. The behavior varies for printf '%10s\n' "$var" in multi-byte locales, there are at least three different outcomes for printf %b '\123'

But in the end, if you stick to the POSIX feature set of printf and don’t try doing anything too fancy with it, you’re out of trouble.

But remember the first argument is the format, so shouldn’t contain variable/uncontrolled data.

A more reliable echo can be implemented using printf, like:

echo() ( # subshell for local scope for $IFS
  IFS=" " # needed for "$*"
  printf '%s\n' "$*"
)

echo_n() (
  IFS=" "
  printf %s "$*"
)

echo_e() (
  IFS=" "
  printf '%b\n' "$*"
)

The subshell (which implies spawning an extra process in most shell implementations) can be avoided using local IFS with many shells, or by writing it like:

echo() {
  if [ "$#" -gt 0 ]; then
     printf %s "$1"
     shift
  fi
  if [ "$#" -gt 0 ]; then
     printf ' %s' "$@"
  fi
  printf '\n'
}

Notes

1. how bash’s echo behaviour can be altered.

With bash, at run time, there are two things that control the behaviour or echo (beside enable -n echo or redefining echo as a function or alias): the xpg_echo bash option and whether bash is in posix mode. posix mode can be enabled if bash is called as sh or if POSIXLY_CORRECT is in the environment or with the the posix option:

Default behaviour on most systems:

$ bash -c 'echo -n "\0101"'
\0101% # the % here denotes the absence of newline character

xpg_echo expands sequences as Unix requires:

$ BASHOPTS=xpg_echo bash -c 'echo "\0101"'
A

It still honours -n and -e (and -E):

$ BASHOPTS=xpg_echo bash -c 'echo -n "\0101"'
A%

With xpg_echo and POSIX mode:

$ env BASHOPTS=xpg_echo POSIXLY_CORRECT=1 bash -c 'echo -n "\0101"'
-n A
$ env BASHOPTS=xpg_echo sh -c 'echo -n "\0101"' # (where sh is a symlink to bash)
-n A
$ env BASHOPTS=xpg_echo SHELLOPTS=posix ARGV0=sh bash -c 'echo -n "\0101"'
-n A
$ env BASHOPTS=xpg_echo SHELLOPTS=posix bash -c 'echo -n "\0101"'
-n A

This time, bash is both POSIX and Unix conformant. Note that in POSIX mode, bash is still not POSIX conformant as it doesn’t output -e in:

 $ env SHELLOPTS=posix bash -c 'echo -e'

 $

The default values for xpg_echo and posix can be defined at compilation time with the --enable-xpg-echo-default and --enable-strict-posix-default options to the configure script. That’s typically what recent versions of OS/X do to build their /bin/sh. No Unix/Linux implementation/distribution in their right mind would typically do that for /bin/bash though.

2. How ksh93’s echo behaviour can be altered.

In ksh93, whether echo expands escape sequences or not and recognises options depends on the content of $PATH.

If $PATH contains a component that contains /5bin or /xpg before the /bin or /usr/bincomponent then it behave the SysV/Unix way (expands sequences, doesn’t accept options). If it finds /ucb or /bsd first, then it behaves the BSD3 way (-e to enable expansion, recognises -n). The default is system dependant, BSD on Debian:

$ ksh93 -c 'echo -n' # default -> BSD (on Debian)
$ PATH=/foo/xpgbar:$PATH ksh93 -c 'echo -n' # /xpg before /bin or /usr/bin -> XPG
-n
$ PATH=/5binary:$PATH ksh93 -c 'echo -n' # /5bin before /bin or /usr/bin -> XPG
-n
$ PATH=/ucb:/foo/xpgbar:$PATH ksh93 -c 'echo -n' # /ucb first -> BSD
$ PATH=/bin:/foo/xpgbar:$PATH ksh93 -c 'echo -n' # /bin before /xpg -> default -> BSD

3. BSD for echo -e?

The reference to BSD for the handling of the -e option is misleading here. All those different and incompatible echo behaviours were all introduced at Bell labs:

  • \n, \0ooo in Programmer’s Word Bench UNIX (based on Unix V6), and the rest (\b, \c…) in Unix System IIIRef.
  • -n in Unix V7 (by Denis RitchieRef)
  • -e in Unix V8 (by Denis RitchieRef)

BSDs just descended from Unix V7. FreeBSD echo still doesn’t support -e, though it does support -n like Unix V7 did.

shareimprove this answer
 
53  
wow. did you, like, write unix? – Eliran Malka Feb 13 ’14 at 14:39
29  
Stephane, a good part of the Bash chops I have learned here and there, I owe them to you. Your commitment to sharing what you know is amazing. I guess the least I could do is say … thank-you, you rock friend. – stefgosselin Feb 27 ’14 at 4:29
5  
A lot of early unix development happened in isolation, and good software engineering principles like ‚when you change the interface, change the name‘ were not applied. – Henk Langeveld Mar 14 ’14 at 7:57
4  
As a note, the one (and maybe only) advantage of having echo expand the \x sequences as opposed to the shell as part of the quoting syntax is that you can then output a NUL byte (another arguably mis-design of Unix is those null-delimited strings, where half the system calls (like execve()) can’t take arbitrary sequences of bytes) – Stéphane Chazelas Aug 4 ’15 at 12:15
 

You might want to use printf for its formatting options. echo is useful when it comes to printing the value of a variable or a (simple) line, but that’s all there is to it. printf can basically do what the C version of it can do.

Example usage and capabilities:

Echo

echo "*** Backup shell script ***"
echo
echo "Runtime: $(date) @ $(hostname)"
echo

printf

vech="bike"
printf "%s\n" "$vech"

Sources:

shareimprove this answer
 
    
@0xC0000022L I stand corrected thanks. I didn’t notice I linked to the wrong site in my rush to answer the question. Thank you for your contribution and the correction. – NlightNFotis Feb 22 ’13 at 20:18
5  
Using echo to print a variable can fail if the value of the variable contains meta-characters. – Keith Thompson Apr 29 ’13 at 15:22

One „advantage“, if you want to call it that, would be that you don’t have to tell it like echo to interpret certain escape sequences such as \n. It knows to interpret them and won’t require an -e to do so.

printf "some\nmulti-lined\ntext\n"

(NB: the last \n is necessary, echo implies it, unless you give the -n option)

versus

echo -e "some\nmulti-lined\ntext"

Note the last \n in printf. At the end of the day it’s a matter of taste and requirements what you use: echo or printf.

shareimprove this answer
 
1  
True for /usr/bin/echo and the bash builtin. The dash, ksh and zsh builtin echo does not need -e switch to expand backslash-escaped characters. – manatwork Feb 23 ’13 at 10:35
    
Why the scare quotes? Your wording implies that it isn’t necessarily a real advantage. – Keith Thompson Apr 29 ’13 at 15:26
1  
@KeithThompson: actually all they are meant to imply is that not everyone may consider it an advantage. – 0xC0000022L Apr 29 ’13 at 15:33
    
Could you expand on that? Why wouldn’t it be an advantage? The phrase „if you want to call it that“ implies pretty strongly that you think it isn’t. – Keith Thompson Apr 29 ’13 at 15:36
2  
Or you could just do printf '%s\n' 'foo\bar. – nyuszika7h Mar 9 ’14 at 17:36

One downside of printf is performance because the built-in shell echo is much faster. This comes into play particularly in Cygwin where each instance of a new command causes heavy Windows overhead. When I changed my echo-heavy program from using /bin/echo to the shell’s echo the performance almost doubled. It’s a trade off between portability and performance. It’s not a slam dunk to always use printf.

shareimprove this answer
 
7  
printf is built in most shells nowadays (bash, dash, ksh, zsh, yash, some pdksh derivatives… so includes the shells typically found on cygwin as well). The only notable exceptions are some pdkshderivatives. – Stéphane Chazelas Nov 7 ’14 at 13:52

Also printf is faster in primitive types with respect to the echo. this is because the the type of the data is specified in the code. the difference in the speed is not recognised as countable factor unless you are coding a driver.

shareimprove this answer
 
3  
What do you mean by “primitive types”? – manatwork Feb 23 ’13 at 14:57
3  
I don’t believe that applies to the printf command. It probably doesn’t apply to the printf function, which has to spend time parsing the format string, – Keith Thompson Apr 29 ’13 at 15:25
    
unless coding a driver phew, I’m lucky I did not write this driver in Bash… – Alois Mahdal Dec 18 ’15 at 5:43
1  
If you’re coding a driver, hopefully you aren’t doing it in a shell script. This answer seems to be about some other topic unrelated to the scope of this question. – Caleb Nov 22 ’16 at 6:16