ela-compil / BACnet

BACnet protocol library for .NET :satellite:
https://www.nuget.org/packages/bacnet/
MIT License
219 stars 96 forks source link

No response to WhoIs on Linux #88

Open moebassist opened 3 years ago

moebassist commented 3 years ago

Hi,

I'm running V2.0.4 on Ubuntu via Mono. If I connect YABE to my PC's ethernet adaptor and the launch my BACnet server app (running on a separate PC Ubuntu 18), everything works well because right after starting the server, an 'Iam(Storage.Deviceid) is executed.

However, when the BACnet server app is running before YABE, it fails to detect a 'Send WhoIs'. I've reviewed incoming packets on the Ubuntu machine and I can confirm the Ethernet adaptor is receiving the broadcast traffic via 192.168.1.255 so there's no firewall/routine issues.

I've added debug code to the 'OnWhoIs' handler which is correctly subscribed to the server (BacNetClient.OnWhoIs += xxxx) that should output 'Who Is' to the console - it never appears, so my guess is the event isn't executing.

Any thoughts?

Thanks!

gralin commented 3 years ago

Hi @moebassist, I don't know how familiar you are with BACnet and/or this library, so just a couple of basic info/questions first:

  1. You don't need to use Mono because the library is also compiled as .NET Standard, so you can simply use it in .NET Core/.NET 5 application directly on Linux.

    fails to detect a 'Send WhoIs'

  2. I guess you mean it fails to detect a response to WhoIs request? In BACnet both all parties can send WhoIs requests and depending on the device id range, other devices should respond with IAm messages. So in your case, you can test this both ways. In you own app, you need to handle incoming WhoIs requests and respond to them with IAm. If you own app wants to send it's own WhoIs requests, it can also do this. In case of YABE, it sends one on the start when you add the transport node, but later you can also send it manually. image
  3. Since you are monitoring OnWhoIs handler in your own app, I assume you want to try to manually force YABE to send this request as described in above.
moebassist commented 3 years ago

Hi @gralin ,

Thanks for the response!

I have an entire HMI application running on .Net 4.7.1 via Mono. It reads data from many Modbus RTU serial devices and acts as a data gateway, publishing the serial data to ModbusTCP and BACnet. The code uses WinForms to run on Linux, hence .Net 4.7.1.

I'm just confirming that I have been sending 'WhoIs' requests from YABE, using 'iptraf' on the Linux PC I see:

UDP (46 bytes) from 192.168.1.76:63630 (my development PC - I'm not sure why the YABE connection uses this port) to 192.168.1.255:47808 (My Linux PC and the port I've started the server on).

As a work around, I'm sending an 'IAm' every 30 seconds from my BACnet server - but it defeats the point of browsing :)

Ant.

gralin commented 3 years ago

Ok. So you can force YABE to use the standard port (47808 or 0xBAC0) for sending and receiving messages like this

image

Maybe this will help? Of course you should receive the WhoIs request in your app whenever you send it from YABE. Maybe a firewall on Linux is blocking it?

moebassist commented 3 years ago

It IS to do with broadcast, and nothing to do with machine configuration.

Looking at BacnetIpUdpProtocolTransport.Open():

_sharedConnis created happily, but _exclusiveConnfails because its endpoint is identical, at which point the function exits....so I can't see how the UDP listener ever worked for both connections?

At the end of this function Bvlc = new BVLC(this); is called to attempt to retrieve the broadcast address - this never executes because of the above.....I also can't understand why this is done AFTER the _sharedConnis created (which should be listening broadcast). I can only image this works because it was written/tested on the same PC, therefore broadcast would 'seemingly work' when YABE sends 'WhoIs'.

For my fix, I've revised the constructor to include the broadcast endpoint and store it in a private variable in the same manner as the IP address:

public BacnetIpUdpProtocolTransport(int port, bool useExclusivePort = false, bool dontFragment = false, int maxPayload = 1472, string localEndpointIp = "",string broadcastEndpointIP="")

And changed BacnetIpUdpProtocolTransport.Open(): for initialising _sharedConnfrom

if (!string.IsNullOrEmpty(_localEndpoint)) ep = new IPEndPoint(IPAddress.Parse(_localEndpoint), SharedPort);

to

if (!string.IsNullOrEmpty(_localEndpoint)) ep = new IPEndPoint(IPAddress.Parse(_broadcastEndpoint), SharedPort);

This means _sharedConn uses the determined TRUE broadcast endpoint (e.g. 192.168.1.255) and _exclusiveConn uses desired endpoint (e.g. 192.168.1.204) - the routine no longer exits due to a binding error on an already used endpoint.

The source mentions this too:

// A lot of problems on Mono (Raspberry) to get the correct broadcast @
// so this method is overridable (this allows the implementation of operating system specific code)
// Marc solution http://stackoverflow.com/questions/8119414/how-to-query-the-subnet-masks-using-mono-on-linux

My application changes the OS IP/Subnet (via an 'options' dialogue) - I don't have to get it from the OS - therefore I wrote a helper function to calculate broadcast address from IP/subnet strings:

        internal static string GetBroadcastAddress(string iP, string networkMask)
        {
            byte[] ipAddressBytes = System.Net.IPAddress.Parse(iP).GetAddressBytes();
            byte[] subnetMaskBytes = System.Net.IPAddress.Parse(networkMask).GetAddressBytes();

            if (ipAddressBytes.Length != subnetMaskBytes.Length) throw new ArgumentException("IP/Mask lengths differ!");
            byte[] bcBytes = new byte[ipAddressBytes.Length];
            for (int i = 0; i < bcBytes.Length; i++)
            {
                bcBytes[i]=(byte)(ipAddressBytes[i] | (subnetMaskBytes[i] ^ 255));
            }
            return new System.Net.IPAddress(bcBytes).ToString();
        }

.....Now it works on Linux. It's not a fix for everyone.

Wascht0 commented 3 years ago

@moebassist Great, thanks a lot. Your solution worked perfectly for me.

moebassist commented 3 years ago

@moebassist Great, thanks a lot. Your solution worked perfectly for me.

Are you experiencing memory leak as per #89 ?

moebassist commented 3 years ago

In addition to the above, the Open() function creates a new BLVC at the last line - which also attempts to determine the broadcast address by iterating through all of the available network adaptors. I've changed this too - (old code simply remarked out)

        // A lot of problems on Mono (Raspberry) to get the correct broadcast @
        // so this method is overridable (this allows the implementation of operating system specific code)
        // Marc solution http://stackoverflow.com/questions/8119414/how-to-query-the-subnet-masks-using-mono-on-linux for instance
        protected virtual BacnetAddress _GetBroadcastAddress()
        {

            // general broadcast by default if nothing better is found
            //var ep = new IPEndPoint(IPAddress.Parse("255.255.255.255"), SharedPort);
            var ep = new IPEndPoint(IPAddress.Parse(_broadcastEndpoint), SharedPort);
            Convert(ep, out var broadcast);
            broadcast.net = 0xFFFF;
            return broadcast;

            //UnicastIPAddressInformation ipAddr = null;

            //if (LocalEndPoint.Address.ToString() == "0.0.0.0")
            //{
            //    ipAddr = GetAddressDefaultInterface();
            //}
            //else
            //{
            //    // restricted local broadcast (directed ... routable)
            //    foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
            //    foreach (var ip in adapter.GetIPProperties().UnicastAddresses)
            //        if (LocalEndPoint.Address.Equals(ip.Address))
            //        {
            //            ipAddr = ip;
            //            break;
            //        }
            //}

            //if (ipAddr != null)
            //{
            //    try
            //    {
            //        var strCurrentIP = ipAddr.Address.ToString().Split('.');
            //        var strIPNetMask = ipAddr.IPv4Mask.ToString().Split('.');
            //        var broadcastStr = new StringBuilder();
            //        for (var i = 0; i < 4; i++)
            //        {
            //            broadcastStr.Append(((byte) (int.Parse(strCurrentIP[i]) | ~int.Parse(strIPNetMask[i]))).ToString());
            //            if (i != 3) broadcastStr.Append('.');
            //        }
            //        ep = new IPEndPoint(IPAddress.Parse(broadcastStr.ToString()), SharedPort);
            //    }
            //    catch
            //    {
            //        // on mono IPv4Mask feature not implemented
            //    }
            //}

            //Convert(ep, out var broadcast);
            //broadcast.net = 0xFFFF;
            //return broadcast;
        }
pocketbroadcast commented 2 years ago

It IS to do with broadcast, and nothing to do with machine configuration.

Windows forwards packets sent to broadcast address to all the addresses bound within the subnet. Linux does not forward the broadcasts anywhere. Thus, you have to explicitly bind to the broadcast address. This is good, since you as developer can decide if you want to listen for broadcasts! In IPv6 there won't even be broadcasts at all!

This means _sharedConn uses the determined TRUE broadcast endpoint (e.g. 192.168.1.255) and _exclusiveConn uses desired endpoint (e.g. 192.168.1.204) - the routine no longer exits due to a binding error on an already used endpoint.

This is highly OS dependent. As stated before, Windows will forward these, thus, you will receive all the broadcasts twice on Windows systems. I fixed this some days ago. Will send a PR as soon as I can.