DevIo.pm IPv6 Support

Begonnen von dev0, 18 August 2017, 12:36:01

Vorheriges Thema - Nächstes Thema

dev0

Mir ist gestern beim Testen eines neuen FHEM Commands aufgefallen, dass DevIo.pm kein IPv6 unterstützt. Wenn ich allerdings die IO::Socket::INET6 Doku richtig verstehe, dann sollte man ::INET durch ::INET6 ersetzen können und alles ist gut... Über diesen Weg lassen sich bei mir alle Varianten aufrufen: "hostname:port" mit A und/oder AAAA DNS Records als auch "IPv4:port" und "[IPv6]:port". Die Reihenfolge, ob IPv4 oder IPv6 zuerst, wird vom Betriebssystem vorgegeben. Bei einem halbwegs aktuellem OS ist das erst v6, dann v4.

In meiner Dual-Stack-Umgebung, mit aktueller IO::Socket::INET6 Version, funktioniert das auch ohne Probleme. Leider habe ich nur eine Versionshistorie zurück bis zur Version 2.53 aus dem Jahr 2008 gefunden. Der SVN Server, der die vollständige History liefern soll, ist nicht (mehr?) erreichbar. Daher prüft der angehangene Patch, ob ::INET6 geladen werden kann und dann noch, ob eine Verbindung mit ::INET6 hergestellt werden kann. Wenn nicht, dann Fallback auf ::INET (ohne IPv6 und ggf. eine Fehlermeldung, wenn der Host nur IPv6 'spricht').

Diese ganze Prüferei geht allerdings auch auf Kosten von max. dem 2-3 fachen blockierenden Timeout, wenn im Extremfall ::INET6 vorhanden, aber ggf. zu alt und dann noch ein A und AAAA DNS Record für einen Hostnamen existiert, der nicht antwortet.

Beim Anwender könnte sich theoretisch eine fehlerhafte IPv6 Konfigurationen, die durch die Verwendung von ::INET6 zum Tragen kommt, auswirken. Das sollte sich mMn aber auf ein verdoppeltes Timeout beschränken, wenn die, im DNS hinterlegte, IPv6 Adresse nicht erreichbar ist.

Den (zu teuren?) Patch bitte nur als Vorschlag/Entwurf/Demo verstehen. Vielleicht gibt es eine viel elegantere Möglichkeit IPv6 Unterstützung in DevIO einzubauen, die ich nicht sehe.

@Rudi: kannst Du Dir vorstellen so etwas in DevIo einzubauen?
@All: Meinungen, Vorschläge ? (wenn Rudi nicht ganz abgeneigt ist)

Sorry, falls zu viel Text ;)


Index: FHEM/DevIo.pm
===================================================================
--- FHEM/DevIo.pm (revision 14902)
+++ FHEM/DevIo.pm (working copy)
@@ -354,7 +354,21 @@
       return undef;     # no double callback: connect is running in bg now

     } else {
-      my $conn = IO::Socket::INET->new(PeerAddr => $dev, Timeout => $timeout);
+      my $conn;
+      my $l = $hash->{devioLoglevel};
+      eval "require IO::Socket::INET6";
+      if(!$@) {
+        $conn = IO::Socket::INET6->new(PeerAddr => $dev, Timeout => $timeout, Multihomed => 1);
+        if (!$conn) {
+          $conn = IO::Socket::INET->new(PeerAddr => $dev, Timeout => $timeout);
+          Log3 $name, ($l ? $l:5), "Perl module IO::Socket::INET6 found, but too old to handle IPv4.";
+        } else {
+          Log3 $name, ($l ? $l:5), "Perl module IO::Socket::INET6 found, new .";
+        }
+      } else {
+        $conn = IO::Socket::INET->new(PeerAddr => $dev, Timeout => $timeout);
+        Log3 $name, ($l ? $l:5), "Perl module IO::Socket::INET6 not installed, "."fallback to ::INET (IPv4 only).\n$@";
+      }
       return "" if(!&$doTcpTail($conn)); # no callback: no doCb
     }

rudolfkoenig

Ich habe nichts gegen eine IPV6 Unterstuetzung.

Ich ueberlege, ob es nicht vertretbar ist, INET einfach durch INET6 zu ersetzen. Wenn das Modul nicht vorhanden ist, sollte der Benutzer es installieren, und wenn IPV6 falsch konfiguriert ist, dann gibt es mit FHEM noch ein Programm, das Probleme hat, man sollte es also richtig konfigurieren. Und wenn IPV6 nicht konfiguriert ist, sollte es auch nicht stoeren.

Warum ist Multihome notwendig?

dev0

#2
ZitatWarum ist Multihome notwendig?
Multihomed ist nicht notwendig, ich hatte vergessen die Option aus dem Patch heraus zu nehmen. Da meine Internetanbindung Multihomed ist, werde ich ggf. noch einmal darauf zurück kommen, wenn ich es weiter getestet habe. Für die IPv6 Unterstützung ist das aber erst einmal nicht relevant.

ZitatIch ueberlege, ob es nicht vertretbar ist, INET einfach durch INET6 zu ersetzen.
Ich habe das gerade mal auf meinem produktiven System getestet:
- IO::Socket::INET6 muss in DevIo.pm via require/use geladen werden, was bei ::INET anscheinend nicht notwendig ist.
- Das Sonos Modul funktionierte nicht mehr. Auf den ersten Blick auch ohne einen Logeintrag dazu.
- Mit der Variante "if (!$conn) { $conn = IO::Socket::INET->new... }" funktioniert ersteinmal auch alles Weitere normal.

Mhh... Ich bin jetzt allerdings auch unsicher, ob diese if Variante auch alle anderen Fälle abdeckt, an die ich nicht gedacht habe bzw. nicht nutze.
Macht es vielleicht Sinn mit IO::Socket::IP zu testen? Gibt es damit schon Erfahrungen?

Edit: IO::Socket::IP funktioniert auch ohne Fallback auf ::INET mit dem Sonos Modul. Vermutlich wird aber kaum jemand das Perl Modul IO::Socket::IP installiert haben.

rudolfkoenig

Zitat- Das Sonos Modul funktionierte nicht mehr. Auf den ersten Blick auch ohne einen Logeintrag dazu.
Danke, damit ist die direkte Umstellung erstmal vom Tisch. Kannst du bitte versuchen rauszufinden, was die Ursache ist?

Ich wuerde gerne IPv6 fuer FHEM transparent einbauen, so dass der Benutzer (ausser IO::Socket::INET6 zu installieren) nichts machen muss. Dafuer muesste ich:
- DevIo und HttpUtils erweitern mit INET6
- HttpUtils_dnsParse AAAA beibringen
Und am Anfang muesste IPv6 per Attribut deaktiviert sein, fuer die Problemfaelle, wie bei Sonos.

dev0

ZitatKannst du bitte versuchen rauszufinden, was die Ursache ist?
Kann ich versuchen, aber vielleicht Reinerlein schon eine Idee dazu?

IO::Socket::IP ist für Dich auch vom Tisch, weil vmtl. im FHEM-Umfeld kaum verbreitet? Wäre das dann nicht ggf. eine Möglichkeit für das nächste (Feature-)Release? Es soll angeblich ein Replacement für ::INET sein (habe ich im Hinterkopf, nicht konkret recherchiert).

rudolfkoenig

ZitatIO::Socket::IP ist für Dich auch vom Tisch,
Nicht unbedingt, hoere aber jetzt zum erstenmal davon, und traue dem deswegen noch nicht.
Laut Doku braucht man dafuer 5.14 (d.h. perl von 2011), oder die Installation wird komplizierter.
Das .deb Paket installiert z.Zt. auch nur INET6 aber nicht IP.
Bin noch unentschieden.

dev0

ZitatDas .deb Paket installiert z.Zt. auch nur INET6 aber nicht IP.
Bist Du sicher? Laut https://packages.debian.org/stretch/all/libio-socket-ip-perl/filelist wird IP.pm installiert. Ich kenne mich aber mit den verschiedenen Debian Versionen nicht so gut aus...

Das Sonos Modul scheint auch IO::Socket::IP zu nutzen (ohne es explizit zu laden?), vielleicht nur dann wenn es installiert ist, da es zumindest schon mal Fehlermeldungen diesbezüglich gab: https://forum.fhem.de/index.php?topic=47758 . Habe aber noch nicht tiefer in den Source geguckt.

rudolfkoenig

Ich habe eine erste Version von INETv6 Support in DevIo.pm und HttpUtils.pm eingecheckt, die synchrone Verbindungen solten unterstuetzt werden, die asynchronen (wie dnsServer und HTTP_NonblockingGet) noch nicht, das ist aufwendiger, als ich dachte.

Zum aktivieren muss man "attr global useInet6" setzen, dann versucht fhem.pl IP:Socket::INET6 zu laden und zu verwenden.
Ich werde es erst dokumentieren, wenn etwas mehr Erfahrung damit vorliegt, und auch nonblocking implementiert ist.

betateilchen

Zitat von: rudolfkoenig am 20 August 2017, 16:25:45
Zum aktivieren muss man "attr global useInet6" setzen,

ist das wieder so ein Ding, wo ein Attribut nur vorhanden sein muss, um etwas zu bewirken oder wird explizit auf "1" geprüft?
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

rudolfkoenig

Es wird auf nicht 0 geprueft:
    if($val || !defined($val)) {

rudolfkoenig

Ich habe in HttpUtils.pm das useInet6 global Attribut auch fuer nonblocking implementiert, inklusive aysnchrones DNS. Ich habe versucht die Aenderungen beim nicht gesetzten useInet6 zu minimieren, und habe viel getestet, bin aber nicht sicher, ob ich nicht etwas uebersehen habe. Mit gesetzten useInet6 wird zunaechst IPv6 versucht, und dann IPv4.

Was nicht geht: eine IPv6 Adresse direkt zu spezifizieren, da ich mit dem Regexp von Boris ueberfordert bin. Falls jemand helfen will, Folgendes muss angepasst werden:

  if($hash->{url} !~
           /^(http|https):\/\/(([^:\/]+):([^:\/]+)@)?([^:\/]+)(:\d+)?(\/.*)$/) {
    return "$hash->{displayurl}: malformed or unsupported URL";
  }

  my ($authstring,$user,$pwd,$port,$host);
  ($hash->{protocol},$authstring,$user,$pwd,$host,$port,$hash->{path})
        = ($1,$2,$3,$4,$5,$6,$7);

und so, dass man fuer $host auch sowas wie [::1] spezifizieren kann.

betateilchen

Schau Dir doch mal das perl Modul Regexp::IPv6 an. Das liefert genau, was Du suchst und es sind nur ein paar Zeilen Coding, die sich sicher auch direkt in FHEM unterbringen lassen. Das wäre sicher auch eine Funktionalität, die man an anderen Stellen innerhalb FHEM künftig noch öfters benötigen wird.

-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

dev0

Mit folgender regex funktioniert auch die Schreibweise [IPv6] als host address, allerdings klemmt es danach noch irgendwo, da die IPv6 Adresse als Hostname interpretiert wird: "DNS: No A record found". Den 'auth string' Teil der Regex habe ich auch noch angepasst, da sonst keine [:/] im Passwort erlaubt waren, aber nur sehr kurz getestet. Die Regex kann man natürlich noch vereinfachen, wenn man nicht bis ins vorletzte Detail prüfen möchte.


  my $rAuth = "([^:\/]+):(.+)@";
  my $rV4   = "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
  my $rV6   = "(?:(?:[0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,7}:|(?:[0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}|(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}|(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}|(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:(?:(?::[0-9a-f]{1,4}){1,6})|:(?:(?::[0-9a-f]{1,4}){1,7}|:)|fe80:(?::[0-9a-f]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))";
  my $rPort = "(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])";
  my $rFqdn = "(?:[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*\.)+[A-Za-z]{2,}";
  my $rHost = "(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])";

  if($hash->{url} !~
           m/^(http|https):\/\/($rAuth)?($rV4|\[$rV6]|$rFqdn|$rHost)(:$rPort)?(\/.*)$/) {
    return "$hash->{displayurl}: malformed or unsupported URL";
  }

  my ($authstring,$user,$pwd,$port,$host);
  ($hash->{protocol},$authstring,$user,$pwd,$host,$port,$hash->{path})
        = ($1,$2,$3,$4,$5,$6,$7);


Die Variablen $1..$7 werden korrekt zugewiesen, zum Debuggen hatte folgende commands eingebaut:

Debug "url: $hash->{url}";
Debug "\$1 prot:$1"; 
Debug "\$2 auth:$2"; 
Debug "\$3 user:$3"; 
Debug "\$4 pass:$4"; 
Debug "\$5 host:$5"; 
Debug "\$6 port:$6"; 
Debug "\$7 path:$7"; 


Ergibt:

2017.08.23 10:08:32.078 1: DEBUG>url: https://ich:123@[2a01:488:66:1000:523:f4e2::1]:80/
2017.08.23 10:08:32.078 1: DEBUG>$1 prot:https
2017.08.23 10:08:32.079 1: DEBUG>$2 auth:ich:123@
2017.08.23 10:08:32.079 1: DEBUG>$3 user:ich
2017.08.23 10:08:32.079 1: DEBUG>$4 pass:123
2017.08.23 10:08:32.079 1: DEBUG>$5 host:[2a01:488:66:1000:523:f4e2::1]
2017.08.23 10:08:32.079 1: DEBUG>$6 port::80
2017.08.23 10:08:32.080 1: DEBUG>$7 path:/
2017.08.23 10:08:32.084 2: getURL ERROR: DNS: No A record found


Auch wenn man direkt nach der Regex die eckigen Klammern von der IPv6 Adresse entfernt, bleibt die Fehlermeldung identisch.
Ein IPv6 Request über DNS Namen funktioniert.
Nebenbei ist mir aufgefallen, dass Hostnamen aus /etc/hosts nicht aufgelöst werden, ob das vorher schon so war habe ich jetzt noch nicht getestet. Ich komme auch erst am WE wieder dazu...

rudolfkoenig

Wenn der Regexp nur so kompliziert geht, dann muss ich selbst nachdenken, sowas spaeter debuggen kann ich nicht mehr.

ZitatHostnamen aus /etc/hosts nicht aufgelöst werden
Das stimmt, wenn dnsServer aktiv ist, da frage ich nur den DNS-Server.
Sonst sollte das funktionieren, da die OS-Routinen verwendet werden.

dev0

Der Regex prüft ob IPs etc auch gültig sind. Wenn es Dir nur ums Splitten geht, dann kann man das vereinfachen.

rudolfkoenig

Ich habe eine deutlich primitivere Version eingebaut, und gleichzeitig den Regexp auf Mehrzeiler umgestellt:
  if($hash->{url} !~ /
      ^(http|https):\/\/                # $1: proto
       (([^:\/]+):([^:\/]+)@)?          # $2: auth, $3:user, $4:password
       ([^:\/]+|\[[0-9a-f:]+\])         # $5: host or IPv6 address
       (:\d+)?                          # $6: port
       (\/.*)$                          # $7: path
    /xi) {


Damit sollte man jetzt auch IPv6 Adressen direkt angeben koennen, Format ist mW ueblich: http://[2a01:4f8:10a:806::2]/index.php
Achtung fuer nicht Eingeweihte: falls ueber eine IP mehrere Websites gehostet werden, dann muss man einen Hostnamen angeben, IP funktioniert nicht.