From f655587bc2c2a11e421f37b739f597646d68e1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20R=C3=BChl?= Date: Wed, 18 Jan 2023 14:56:17 +0100 Subject: [PATCH] feat(plc4go/bacnet): implement BIPForeignApplication --- plc4go/internal/bacnetip/ApplicationModule.go | 103 ++++++- .../bacnetip/BACnetVirtualLinkLayerService.go | 291 ++++++++++++++++++ 2 files changed, 393 insertions(+), 1 deletion(-) diff --git a/plc4go/internal/bacnetip/ApplicationModule.go b/plc4go/internal/bacnetip/ApplicationModule.go index 8e6ba0a1216..2089a635526 100644 --- a/plc4go/internal/bacnetip/ApplicationModule.go +++ b/plc4go/internal/bacnetip/ApplicationModule.go @@ -213,7 +213,7 @@ type Application struct { } func NewApplication(localDevice *LocalDeviceObject, deviceInfoCache *DeviceInfoCache, aseID *int) (*Application, error) { - log.Debug().Msgf("NewApplication localDevice=%v localAddress=%v deviceInfoCache=%s aseID=%d", localDevice, deviceInfoCache, aseID) + log.Debug().Msgf("NewApplication localDevice=%v deviceInfoCache=%s aseID=%d", localDevice, deviceInfoCache, aseID) a := &Application{} var err error a.ApplicationServiceElement, err = NewApplicationServiceElement(aseID, a) @@ -616,3 +616,104 @@ func (b *BIPSimpleApplication) Close() error { // pass to the multiplexer, then down to the sockets return b.mux.Close() } + +type BIPForeignApplication struct { + *ApplicationIOController + *WhoIsIAmServices + *ReadWritePropertyServices + localAddress Address + asap *ApplicationServiceAccessPoint + smap *StateMachineAccessPoint + nsap *NetworkServiceAccessPoint + nse *NetworkServiceElement + bip *BIPForeign + annexj *AnnexJCodec + mux *UDPMultiplexer +} + +func NewBIPForeignApplication(localDevice *LocalDeviceObject, localAddress Address, bbmdAddress *Address, bbmdTTL *int, deviceInfoCache *DeviceInfoCache, aseID *int) (*BIPForeignApplication, error) { + b := &BIPForeignApplication{} + var err error + b.ApplicationIOController, err = NewApplicationIOController(localDevice, deviceInfoCache, aseID) + if err != nil { + return nil, errors.Wrap(err, "error creating io controller") + } + b.WhoIsIAmServices, err = NewWhoIsIAmServices(b) + if err != nil { + return nil, errors.Wrap(err, "error WhoIs/IAm services") + } + b.ReadWritePropertyServices, err = NewReadWritePropertyServices() + if err != nil { + return nil, errors.Wrap(err, "error read write property services") + } + + b.localAddress = localAddress + + // include a application decoder + b.asap, err = NewApplicationServiceAccessPoint(nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating application service access point") + } + + // pass the device object to the state machine access point, so it can know if it should support segmentation + b.smap, err = NewStateMachineAccessPoint(localDevice, deviceInfoCache, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating state machine access point") + } + + // pass the device object to the state machine access point so it # can know if it should support segmentation + // Note: deviceInfoCache already passed above, so we don't need to do it again here + + // a network service access point will be needed + b.nsap, err = NewNetworkServiceAccessPoint(nil, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating network service access point") + } + + // give the NSAP a generic network layer service element + b.nse, err = NewNetworkServiceElement(nil) + if err != nil { + return nil, errors.Wrap(err, "error creating new network service element") + } + if err := bind(b.nse, b.nsap); err != nil { + return nil, errors.Wrap(err, "error binding network stack") + } + + // bind the top layers + if err := bind(b, b.asap, b.smap, b.nsap); err != nil { + return nil, errors.New("error binding top layers") + } + + // create a generic BIP stack, bound to the Annex J server on the UDP multiplexer + b.bip, err = NewBIPForeign(bbmdAddress, bbmdTTL, nil, nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating new bip") + } + b.annexj, err = NewAnnexJCodec(nil, nil) + if err != nil { + return nil, errors.Wrap(err, "error creating new annex j codec") + } + b.mux, err = NewUDPMultiplexer(b.localAddress, true) + if err != nil { + return nil, errors.Wrap(err, "error creating new udp multiplexer") + } + + // bind the bottom layers + if err := bind(b.bip, b.annexj, b.mux.annexJ); err != nil { + return nil, errors.Wrap(err, "error binding bottom layers") + } + + // bind the BIP stack to the network, no network number + if err := b.nsap.bind(b.bip, nil, &b.localAddress); err != nil { + return nil, err + } + + return b, nil +} + +func (b *BIPForeignApplication) Close() error { + log.Debug().Msg("close socket") + + // pass to the multiplexer, then down to the sockets + return b.mux.Close() +} diff --git a/plc4go/internal/bacnetip/BACnetVirtualLinkLayerService.go b/plc4go/internal/bacnetip/BACnetVirtualLinkLayerService.go index 76dca48357d..706779df360 100644 --- a/plc4go/internal/bacnetip/BACnetVirtualLinkLayerService.go +++ b/plc4go/internal/bacnetip/BACnetVirtualLinkLayerService.go @@ -23,6 +23,7 @@ import ( readWriteModel "github.com/apache/plc4x/plc4go/protocols/bacnetip/readwrite/model" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "time" ) type _MultiplexClient struct { @@ -446,3 +447,293 @@ func (b *BIPSimple) Confirmation(pdu _PDU) error { return nil } } + +type BIPForeign struct { + *BIPSAP + *Client + *Server + *OneShotTask + registrationStatus int + bbmdAddress *Address + bbmdTimeToLive *int + registrationTimeoutTask *OneShotFunctionTask +} + +func NewBIPForeign(addr *Address, ttl *int, sapID *int, cid *int, sid *int) (*BIPForeign, error) { + log.Debug().Msgf("NewBIPForeign addr=%s ttl=%d sapID=%d cid=%d sid=%d", addr, ttl, sapID, cid, sid) + b := &BIPForeign{} + bipsap, err := NewBIPSAP(sapID, b) + if err != nil { + return nil, errors.Wrap(err, "error creating bisap") + } + b.BIPSAP = bipsap + client, err := NewClient(cid, b) + if err != nil { + return nil, errors.Wrap(err, "error creating client") + } + b.Client = client + server, err := NewServer(sid, b) + if err != nil { + return nil, errors.Wrap(err, "error creating server") + } + b.Server = server + b.OneShotTask = NewOneShotTask(b, nil) + + // -2=unregistered, -1=not attempted or no ack, 0=OK, >0 error + b.registrationStatus = -1 + + // clear the BBMD address and time-to-live + b.bbmdAddress = nil + b.bbmdTimeToLive = nil + + // used in tracking active registration timeouts + b.registrationTimeoutTask = OneShotFunction(b._registration_expired) + + // registration provided + if addr != nil { + // a little error checking + if ttl == nil { + return nil, errors.New("BBMD address and time-to-live must both be specified") + } + + if err := b.register(*addr, *ttl); err != nil { + return nil, errors.Wrap(err, "error registering") + } + } + + return b, nil +} + +func (b *BIPForeign) Indication(pdu _PDU) error { + log.Debug().Msgf("Indication %s", pdu) + + // check for local stations + switch pdu.GetPDUDestination().AddrType { + case LOCAL_STATION_ADDRESS: + // make an original unicast PDU + xpdu := readWriteModel.NewBVLCOriginalUnicastNPDU(pdu.GetMessage().(readWriteModel.NPDU), 0) + log.Debug().Msgf("xpdu:\n%s", xpdu) + + // send it downstream + return b.Request(NewPDUFromPDUWithNewMessage(pdu, xpdu)) + case LOCAL_BROADCAST_ADDRESS: + // check the BBMD registration status, we may not be registered + if b.registrationStatus != 0 { + log.Debug().Msg("packet dropped, unregistered") + return nil + } + + // make an original broadcast PDU + xpdu := readWriteModel.NewBVLCOriginalBroadcastNPDU(pdu.GetMessage().(readWriteModel.NPDU), 0) + + log.Debug().Msgf("xpdu:\n%s", xpdu) + + // send it downstream + return b.Request(NewPDUFromPDUWithNewMessage(pdu, xpdu)) + default: + return errors.Errorf("invalid destination address: %s", pdu.GetPDUDestination()) + } +} + +func (b *BIPForeign) Confirmation(pdu _PDU) error { + log.Debug().Msgf("Confirmation %s", pdu) + + switch msg := pdu.GetMessage().(type) { + // check for a registration request result + case readWriteModel.BVLCResultExactly: + // if we are unbinding, do nothing + if b.registrationStatus == -2 { + return nil + } + + // make sure we have a bind request in process + + // make sure the result is from the bbmd + + if !pdu.GetPDUSource().Equals(b.bbmdAddress) { + log.Debug().Msg("packet dropped, not from the BBMD") + return nil + } + // save the result code as the status + b.registrationStatus = int(msg.GetCode()) + + // If successful, track registration timeout + if b.registrationStatus == 0 { + b._start_track_registration() + } + + return nil + case readWriteModel.BVLCOriginalUnicastNPDUExactly: + // build a vanilla PDU + xpdu := NewPDU(msg.GetNpdu(), WithPDUSource(pdu.GetPDUSource()), WithPDUDestination(pdu.GetPDUDestination())) + log.Debug().Msgf("xpdu: %s", xpdu) + + // send it upstream + return b.Response(xpdu) + case readWriteModel.BVLCForwardedNPDUExactly: + // check the BBMD registration status, we may not be registered + if b.registrationStatus != 0 { + log.Debug().Msg("packet dropped, unregistered") + return nil + } + + // make sure the forwarded PDU from the bbmd + if !pdu.GetPDUSource().Equals(b.bbmdAddress) { + log.Debug().Msg("packet dropped, not from the BBMD") + return nil + } + + // build a PDU with the source from the real source + ip := msg.GetIp() + port := msg.GetPort() + source, err := NewAddress(append(ip, uint16ToPort(port)...)) + if err != nil { + return errors.Wrap(err, "error building a ip") + } + xpdu := NewPDU(msg.GetNpdu(), WithPDUSource(source), WithPDUDestination(NewLocalBroadcast(nil))) + log.Debug().Msgf("xpdu: %s", xpdu) + + // send it upstream + return b.Response(xpdu) + case readWriteModel.BVLCReadBroadcastDistributionTableAckExactly: + // send this to the service access point + return b.SapResponse(pdu) + case readWriteModel.BVLCReadForeignDeviceTableAckExactly: + // send this to the service access point + return b.SapResponse(pdu) + case readWriteModel.BVLCWriteBroadcastDistributionTableExactly: + // build a response + result := readWriteModel.NewBVLCResult(readWriteModel.BVLCResultCode_WRITE_BROADCAST_DISTRIBUTION_TABLE_NAK) + xpdu := NewPDU(result, WithPDUDestination(pdu.GetPDUSource())) + + // send it downstream + return b.Request(xpdu) + case readWriteModel.BVLCReadBroadcastDistributionTableExactly: + // build a response + result := readWriteModel.NewBVLCResult(readWriteModel.BVLCResultCode_READ_BROADCAST_DISTRIBUTION_TABLE_NAK) + xpdu := NewPDU(result, WithPDUDestination(pdu.GetPDUSource())) + + // send it downstream + return b.Request(xpdu) + case readWriteModel.BVLCRegisterForeignDeviceExactly: + // build a response + result := readWriteModel.NewBVLCResult(readWriteModel.BVLCResultCode_REGISTER_FOREIGN_DEVICE_NAK) + xpdu := NewPDU(result, WithPDUDestination(pdu.GetPDUSource())) + + // send it downstream + return b.Request(xpdu) + case readWriteModel.BVLCReadForeignDeviceTableExactly: + // build a response + result := readWriteModel.NewBVLCResult(readWriteModel.BVLCResultCode_READ_FOREIGN_DEVICE_TABLE_NAK) + xpdu := NewPDU(result, WithPDUDestination(pdu.GetPDUSource())) + + // send it downstream + return b.Request(xpdu) + case readWriteModel.BVLCDeleteForeignDeviceTableEntryExactly: + // build a response + result := readWriteModel.NewBVLCResult(readWriteModel.BVLCResultCode_DELETE_FOREIGN_DEVICE_TABLE_ENTRY_NAK) + xpdu := NewPDU(result, WithPDUDestination(pdu.GetPDUSource())) + + // send it downstream + return b.Request(xpdu) + case readWriteModel.BVLCDistributeBroadcastToNetworkExactly: + // build a response + result := readWriteModel.NewBVLCResult(readWriteModel.BVLCResultCode_DISTRIBUTE_BROADCAST_TO_NETWORK_NAK) + xpdu := NewPDU(result, WithPDUDestination(pdu.GetPDUSource())) + + // send it downstream + return b.Request(xpdu) + case readWriteModel.BVLCOriginalBroadcastNPDUExactly: + log.Debug().Msg("packet dropped") + return nil + default: + log.Warn().Msgf("invalid pdu type %T", msg) + return nil + } +} + +// register starts the foreign device registration process with the given BBMD. +// +// Registration will be renewed periodically according to the ttl value +// until explicitly stopped by a call to `unregister`. +func (b *BIPForeign) register(addr Address, ttl int) error { + // a little error checking + if ttl <= 0 { + return errors.New("time-to-live must be greater than zero") + } + + // save the BBMD address and time-to-live + b.bbmdAddress = &addr + b.bbmdTimeToLive = &ttl + + // install this task to do registration renewal according to the TTL + // and stop tracking any active registration timeouts + var taskTime time.Time + b.InstallTask(&taskTime, nil) + b._stop_track_registration() + return nil +} + +// unregister stops the foreign device registration process. +// +// Immediately drops active foreign device registration and stops further +// registration renewals. +func (b *BIPForeign) unregister() { + pdu := NewPDU(readWriteModel.NewBVLCRegisterForeignDevice(0), WithPDUDestination(b.bbmdAddress)) + + // send it downstream + if err := b.Request(pdu); err != nil { + log.Debug().Err(err).Msg("error sending request") + return + } + + // change the status to unregistered + b.registrationStatus = -2 + + // clear the BBMD address and time-to-live + b.bbmdAddress = nil + b.bbmdTimeToLive = nil + + // unschedule registration renewal & timeout tracking if previously + // scheduled + b.SuspendTask() + b._stop_track_registration() +} + +// processTask is called when the registration request should be sent to the BBMD. +func (b *BIPForeign) processTask() error { + pdu := NewPDU(readWriteModel.NewBVLCRegisterForeignDevice(uint16(*b.bbmdTimeToLive)), WithPDUDestination(b.bbmdAddress)) + + // send it downstream + if err := b.Request(pdu); err != nil { + return errors.Wrap(err, "error sending request") + } + + // schedule the next registration renewal + var delta = time.Duration(*b.bbmdTimeToLive) * time.Second + b.InstallTask(nil, &delta) + return nil +} + +// _start_track_registration From J.5.2.3 Foreign Device Table Operation (paraphrasing): if a +// foreign device does not renew its registration 30 seconds after its +// TTL expired then it will be removed from the BBMD's FDT. +// +// Thus, if we're registered and don't get a response to a subsequent +// renewal request 30 seconds after our TTL expired then we're +// definitely not registered anymore. +func (b *BIPForeign) _start_track_registration() { + var delta = time.Duration(*b.bbmdTimeToLive)*time.Second + (30 * time.Second) + b.registrationTimeoutTask.InstallTask(nil, &delta) +} + +func (b *BIPForeign) _stop_track_registration() { + b.registrationTimeoutTask.SuspendTask() +} + +// _registration_expired is called when detecting that foreign device registration has definitely expired. +func (b *BIPForeign) _registration_expired() error { + b.registrationStatus = -1 // Unregistered + b._stop_track_registration() + return nil +}