diff --git a/fhem/FHEM/TcpServerUtils.pm b/fhem/FHEM/TcpServerUtils.pm index 86e8ba544..382b67d5d 100644 --- a/fhem/FHEM/TcpServerUtils.pm +++ b/fhem/FHEM/TcpServerUtils.pm @@ -8,11 +8,14 @@ use IO::Socket; use vars qw($SSL_ERROR); sub -TcpServer_Open($$$) +TcpServer_Open($$$;$) { - my ($hash, $port, $global) = @_; + my ($hash, $port, $global, $multicast) = @_; my $name = $hash->{NAME}; + return 'multicast not supported without SO_REUSEPORT' + if($multicast && !defined(&SO_REUSEPORT)); + if($port =~ m/^IPV6:(\d+)$/i) { $port = $1; eval "require IO::Socket::INET6; use Socket6;"; @@ -26,7 +29,7 @@ TcpServer_Open($$$) my $lh = ($global ? ($global eq "global"? undef : $global) : ($hash->{IPV6} ? "::1" : "127.0.0.1")); - my @opts = ( + my %opts = ( Domain => ($hash->{IPV6} ? AF_INET6() : AF_UNSPEC), # Linux bug / #126448 LocalHost => $lh, LocalPort => $port, @@ -34,10 +37,18 @@ TcpServer_Open($$$) Blocking => ($^O =~ /Win/ ? 1 : 0), # Needed for .WRITEBUFFER@darwin ReuseAddr => 1 ); + + if($multicast) { + $opts{ReusePort} = 1; + $opts{Proto} = 'udp'; + delete($opts{Listen}); + $hash->{MULTICAST} = $multicast; + } + readingsSingleUpdate($hash, "state", "Initialized", 0); $hash->{SERVERSOCKET} = $hash->{IPV6} ? - IO::Socket::INET6->new(@opts) : - IO::Socket::INET->new(@opts); + IO::Socket::INET6->new(%opts) : + IO::Socket::INET->new(%opts); if(!$hash->{SERVERSOCKET}) { return "$name: Can't open server port at $port: $!"; @@ -51,12 +62,192 @@ TcpServer_Open($$$) return undef; } +sub +TcpServer_SetLoopbackMode($$) +{ + my ($hash, $loopback) = @_; + my $name = $hash->{NAME}; + my $sock = $hash->{SERVERSOCKET}; + + my $old; + if( !$hash->{IPV6} && $sock->sockdomain() == AF_INET ) { + my $packed = getsockopt($sock, Socket::IPPROTO_IP, + Socket::IP_MULTICAST_LOOP); + if( !$packed ) { + Log3 $name, 1, "$name: failed to get loopback mode: $!"; + return undef; + } + $old = unpack("I", $packed); + + if( !setsockopt($sock, Socket::IPPROTO_IP, + Socket::IP_MULTICAST_LOOP, pack("I", $loopback ) ) ) { + Log3 $name, 1, "$name: could not set loopback mode: $!"; + return undef; + } + + } elsif( !$hash->{IPV6} && $sock->sockdomain() == AF_INET6 ) { + my $packed = getsockopt($sock, Socket::IPPROTO_IPV6, + Socket::IPV6_MULTICAST_LOOP); + if( !$packed ) { + Log3 $name, 1, "$name: failed to get loopback mode: $!"; + return undef; + } + $old = unpack("I", $packed); + + if( setsockopt($sock, Socket::IPPROTO_IPV6, + Socket::IPV6_MULTICAST_LOOP, pack("I", $loopback ) ) ) { + Log3 $name, 1, "$name: could not set loopback mode: $!"; + return undef; + } + + } else { + Log3 $name, 1, + "$name: TcpServer_SetLoopbackMode failed: unsupported socket family"; + return undef; + } + + return $old; +} + +sub +TcpServer_MCastAdd($$) +{ + my ($hash, $addr) = @_; + my $name = $hash->{NAME}; + my $sock = $hash->{SERVERSOCKET}; + + $hash->{ADDR} = $addr; + + # disable loopback + TcpServer_SetLoopbackMode($hash, 0); + + # add multicast address + if(!$hash->{IPV6} && $sock->sockdomain() == AF_INET) { + # should we allow to specify the interfache instead of using any? + my $ip_mreq = Socket::pack_ip_mreq( inet_aton( $addr ), INADDR_ANY ); + + setsockopt($sock, Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, $ip_mreq ) + or return "$name: could not set IP_ADD_MEMBERSHIP socket option: $!"; + + } elsif($hash->{IPV6} && $sock->sockdomain() == AF_INET6) { + # should we allow to specify the interfache instead of using any? + my $ipv6_mreq = Socket::pack_ipv6_mreq( inet_pton( AF_INET6, $addr ), 0 ); + + setsockopt($sock, Socket::IPPROTO_IPV6, Socket::IPV6_JOIN_GROUP, $ipv6_mreq ) + or return "$name: could not set IPV6_JOIN_GROUP socket option: $!"; + + } else { + return("$name: TcpServer_MCastAdd failed: unsupported socket family" ); + + } + + readingsSingleUpdate($hash, "state", "Multicast listen", 0); + + return undef; +} + +sub +TcpServer_MCastRecv($$$;$) +{ + my ($hash, undef, $length, $flags) = @_; + my $name = $hash->{NAME}; + my $sock = $hash->{SERVERSOCKET}; + + my $sockaddr = $sock->recv($_[1], $length, $flags); + if(!$hash->{IPV6} && $sock->sockdomain() == AF_INET) { + my ($peer_port, $addr) = Socket::unpack_sockaddr_in($sockaddr); + my $peer_host = inet_ntoa($addr); + return $peer_host, $peer_port; + + } elsif($hash->{IPV6} && $sock->sockdomain() == AF_INET6) { + my ($peer_port, $addr) = Socket::unpack_sockaddr_in6($sockaddr); + my $peer_host = inet_ntop(AF_INET6(),$addr); + return $peer_host, $peer_port; + + } else { + Log3 $name, 1, "$name: TcpServer_MCastRecv failed: unsupported socket family"; + return undef; + } +} + +sub +TcpServer_MCastSend($$;$$) +{ + my ($hash, $data, $addr, $port) = @_; + my $name = $hash->{NAME}; + my $sock = $hash->{SERVERSOCKET}; + + $addr = $hash->{ADDR} if( !$addr ); + $port = $hash->{PORT} if( !$port ); + + if( !$addr ) { + Log3 $name, 1, "$name: TcpServer_MCastSend failed: address unknown"; + return undef; + } + if( !$port ) { + Log3 $name, 1, "$name: TcpServer_MCastSend failed: port unknown"; + return undef; + } + + if(!$hash->{IPV6} && $sock->sockdomain() == AF_INET) { + my $sockaddr = Socket::pack_sockaddr_in($port, inet_aton($addr)); + return $sock->send($data,0,$sockaddr); + + } elsif($hash->{IPV6} && $sock->sockdomain() == AF_INET6) { + my $sockaddr = Socket::pack_sockaddr_in6($port, inet_pton($addr)); + return $sock->send($data,0,$sockaddr); + + } else { + Log3 $name, 1,"$name: TcpServer_MCastSend failed: unsupported socket family"; + return undef; + } + +} + +sub +TcpServer_MCastRemove($$) +{ + my ($hash, $addr) = @_; + my $name = $hash->{NAME}; + my $sock = $hash->{SERVERSOCKET}; + + delete $hash->{ADDR}; + + if(!$hash->{IPV6} && $sock->sockdomain() == AF_INET) { + # should we allow to specify the interfache instead of using any? + my $ip_mreq = Socket::pack_ip_mreq( inet_aton( $addr ), INADDR_ANY ); + + setsockopt($sock, Socket::IPPROTO_IP, Socket::IP_DROP_MEMBERSHIP, $ip_mreq ) + or return "$name: could not set IP_DROP_MEMBERSHIP socket option: $!"; + + } elsif($hash->{IPV6} && $sock->sockdomain() == AF_INET6) { + # should we allow to specify the interfache instead of using any? + my $ipv6_mreq = Socket::pack_ipv6_mreq( inet_pton( AF_INET6, $addr ), 0 ); + + setsockopt($sock, Socket::IPPROTO_IPV6, Socket::IPV6_LEAVE_GROUP, $ipv6_mreq) + or return "$name: could not set IPV6_LEAVE_GROUP socket option: $!"; + + } else { + return("$name: TcpServer_MCastRemove failed: unsupported socket family" ); + + } + + readingsSingleUpdate($hash, "state", "Multicast listen stopped", 0); + + return undef; +} + sub TcpServer_Accept($$) { my ($hash, $type) = @_; - my $name = $hash->{NAME}; + + if($hash->{MULTICAST}) { + Log3 $name, 1, "$name: can't accept on a mutlicast socket"; + return undef; + } + my @clientinfo = $hash->{SERVERSOCKET}->accept(); if(!@clientinfo) { Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN); @@ -383,4 +574,49 @@ TcpServer_WriteBlocking($$) return 1; # success } +=pod + +Multicast: +(https://forum.fhem.de/index.php/topic,126290.msg1209591.html#msg1209591) +verwendet wird es so: +- das socket mit gesetztem optionalen vierten paramter von TcpServer_Open + erzeugen: d.h. im define oder sonst wo mit my $ret = TcpServer_Open($hash, + '5353', '0.0.0.0', 1); initialisieren. statt der 0.0.0.0 kann man auch die + multicast adresse angeben, ich bin mir nicht sicher was richtiger ist und ob + es einen unterschied macht. funktionieren tut bei mir beides. +- TcpServer_Open schaltet automatisch den loopback mode aus. d.h. man empfängt + seine eigenen daten nicht. ich vermute das ist der normalfall. mit + TcpServer_SetLoopbackMode($hash,[0|1]); kann man das ändern wenn man es + braucht. +- mit TcpServer_MCastAdd die multicast adresse zum socket explizit hinzufügen: + z.b.: TcpServer_MCastAdd($hash,'224.0.0.251'); erst ab jetzt bekommt man auch + tatsächlich daten. +- im (device) hash wird für jedes udp packet die ReadFn aufgerufen. dort kann + man mit TcpServer_MCastRecv dann die empfangenen daten abholen: + my($peer_host, $peer_port) = TcpServer_MCastRecv($hash,$data,$length); + + alternativ kann man auch die low leven routinen direkt aufrufen: + - my $sockaddr = $hash->{SERVERSOCKET}->recv($data, 4096); + der empfang geht über SERVERSOCKET, nicht wie bei tcp über CD, es wird auch + kein accept verwendet + - aus $sockaddr kann man sich die gegenstelle zum udp packet holen falls man + die braucht: + my ($peer_port, $addr) = Socket::sockaddr_in($sockaddr); + my $peer_host = inet_ntoa($addr); +- eigene daten sendet man mit TcpServer_MCastSend: + TcpServer_MCastSend($hash,$data[,$host[,$port]]); hier bei wird der port aus + dem TcpServer_Open und die adresse aus TcpServer_MCastAdd verwendet. + alternativ kann man beides auch beim aufruf mitgeben. +- mit TcpServer_MCastRemove kann man die multicast adresse auch wieder vom + socket entfernen: + z.b.: TcpServer_MCastRemove($hash,'224.0.0.251'); + das socket empfängt dann keine daten für diese adresse mehr. +- mit wechselweisem TcpServer_MCastAdd und TcpServer_MCastRemove kann man auch + zeitweise zwischen empfangen und ignorieren hin und her wechseln. z.b. als + reaktion auf disable. +- mit TcpServer_Close($hash); das ganze am ende z.b. in der UndefFn wieder zu + machen. + +=cut + 1;