Bannet协议学习记录

本文介绍了如何使用Go语言配合gobacnet库实现Bancnet协议的数据发送和读取,包括通过BACnetDeviceSimulator模拟设备、使用BACnetScan工具进行设备发现以及处理WhoIs和ReadProperty请求。作者还提供了代码示例和WireShark在报文分析中的应用。
摘要由CSDN通过智能技术生成

记录(一):go语言实现Bancnet协议数据发送模拟与读取

准备工作

  1. 虚拟机
  2. bacnet scan(本机安装)
  3. BACnet Device Simulator(虚拟机安装)
  4. vscode
  5. bacnet的go语言库:github.com/alexbeltran/gobacnet

执行工作

虚拟机中,用模拟软件新建虚拟设备

在这里插入图片描述

在本机执行bacnetScan

  1. 选中IP,然后点击小眼睛,搜索设备,此时发送whois请求,返回iam请求
    在这里插入图片描述

  2. 选中一个设备,右键搜索点,此时应该实行的是readProperty请求,返回设备下的object对象
    在这里插入图片描述

  3. 然后选中一个object,右键-》读对象属性,显示该对象的属性,其中presentValue是比较关注的属性。
    在这里插入图片描述
    这是在bacnetScan工具中,读取设备的流程,可以看到设备的一些信息,在simulator中的log可以看到,scan执行之后相互发送的报文,但是看起来有点混乱,推荐使用WireShark软件检测报文的交互,可以看到非常详细的报文解释,在程序中可以对照着scan发送的报文,来修改代码。
    在这里插入图片描述

代码编程

  • 引用前面提到的库,开始读取设备,注!!!要关掉本机的BacnetScan,因为会占端口,虚拟机和本机的防火墙都打开,入站规则,出战规则都加上!!!否则会读取不到设备信息。
package main

import (
	"fmt"
	"log"

	"github.com/alexbeltran/gobacnet"
	"github.com/alexbeltran/gobacnet/property"
	"github.com/alexbeltran/gobacnet/types"
)

func main() {
	cli, err := gobacnet.NewClient("VMware Network Adapter VMnet8", gobacnet.DefaultPort)
	if err != nil {
		fmt.Println("123")
	}
	defer cli.Close()
	resp, err1 := cli.WhoIs(-1, -1)
	if err1 != nil {
		fmt.Println("读取设备有误")
	}
	fmt.Println("设备信息", resp)
	for _, d := range resp {
		dest := d
		rp := types.ReadPropertyData{
			Object: types.Object{
				ID: types.ObjectID{
					Type:     8, //types.DeviceType,
					Instance: types.ObjectInstance(dest.ID.Instance),
				},
				Properties: []types.Property{
					types.Property{
						Type:       property.ObjectList,
						ArrayIndex: gobacnet.ArrayAll,
					},
				},
			},
		}

		out, err := cli.ReadProperty(dest, rp)
		if err != nil {
			log.Fatal(err)
			return
		}
		fmt.Println(out)
		//读数据
		ids, ok := out.Object.Properties[0].Data.([]interface{})

		if !ok {
			fmt.Println("Unable to get object list")
			continue
		}

		rpm := types.ReadMultipleProperty{}
		rpm.Objects = make([]types.Object, len(ids))
		for i, raw_id := range ids {
			id, ok := raw_id.(types.ObjectID)
			if !ok {
				log.Println("Unable to read object id %v", raw_id)
				return
			}
			rpm.Objects[i].ID = id

			rpm.Objects[i].Properties = []types.Property{
				types.Property{
					Type:       property.ObjectName,
					ArrayIndex: gobacnet.ArrayAll,
				},

				types.Property{
					Type:       property.PresentValue,
					ArrayIndex: gobacnet.ArrayAll,
				},
			}
			x, err := cli.ReadMultiProperty(dest, rpm)
			if err != nil {
				log.Println(err)
			} else {
				fmt.Println(x)
			}

		}

	}

}
  • 使用库源代码的例子,会读取不到很多信息,通过wireShark会发现,请求报文有异常,解包也有问题,所以对照报文进行修改自己的代码,发送正确的报文请求,就能获取信息。

坑记

  • 建立连接的时候,不知道填什么接口,所以需要通过代码调试,根据代码来填写。
  • 发送whois请求,接受iam时,解包错误,修改listen.go的handmsg函数,添加a := npdu.Source解出iam的源地址网络号,然后将a地址与源代码中iam解包的地址结合。这样后续的readproperty函数和readmultiproperty函数就可以使用返回对象中的地址属性来给请求的目的地址赋值。
func (c *Client) handleMsg(src *net.UDPAddr, b []byte) {
	var header bactype.BVLC
	var npdu bactype.NPDU
	var apdu bactype.APDU

	dec := encoding.NewDecoder(b)
	err := dec.BVLC(&header)
	if err != nil {
		c.log.Error(err)
		return
	}

	if header.Function == bactype.BacFuncBroadcast || header.Function == bactype.BacFuncUnicast || header.Function == bactype.BacFuncForwardedNPDU {
		// Remove the header information
		b = b[mtuHeaderLength:]
		err = dec.NPDU(&npdu)
		a := npdu.Source

		if err != nil {
			return
		}

		if npdu.IsNetworkLayerMessage {
			c.log.Debug("Ignored Network Layer Message")
			return
		}

		// We want to keep the APDU intact so we will get a snapshot before decoding
		// further
		send := dec.Bytes()
		err = dec.APDU(&apdu)
		if err != nil {
			return
		}
		switch apdu.DataType {
		case bactype.UnconfirmedServiceRequest:
			if apdu.UnconfirmedService == bactype.ServiceUnconfirmedIAm {
				c.log.Debug("Received IAm Message")
				dec = encoding.NewDecoder(apdu.RawData)
				var iam bactype.IAm

				err = dec.IAm(&iam)

				// For whatever reason, the IP section won't be populated until
				// we set the type.
				src.IP = src.IP.To4()

				iam.Addr = bactype.UDPToAddress(src)

				if a != nil {
					iam.Addr.Net = a.Net
					iam.Addr.Len = a.Len
					iam.Addr.Adr = a.Adr
				} else {
					c.log.Errorf("npdu.Source is nil")
					return //不管默认设备
				}
				//iam.Addr = *a //改为iam解码的地址

				if err != nil {
					c.log.Error(err)
					return
				}
				c.utsm.Publish(int(iam.ID.Instance), iam)
			} else if apdu.UnconfirmedService == bactype.ServiceUnconfirmedWhoIs {
				dec := encoding.NewDecoder(apdu.RawData)
				var low, high int32
				dec.WhoIs(&low, &high)
				// For now we are going to ignore who is request.
				//log.WithFields(log.Fields{"low": low, "high": high}).Debug("WHO IS Request")
			} else {
				c.log.Errorf("Unconfirmed: %d %v", apdu.UnconfirmedService, apdu.RawData)
			}
		case bactype.ComplexAck:
			c.log.Debug("Received Complex Ack")
			err := c.tsm.Send(int(apdu.InvokeId), send)
			if err != nil {
				return
			}
		case bactype.ConfirmedServiceRequest:
			c.log.Debug("Received  Confirmed Service Request")
			err := c.tsm.Send(int(apdu.InvokeId), send)
			if err != nil {
				return
			}
		case bactype.Error:
			err := fmt.Errorf("Error Class %d Code %d", apdu.Error.Class, apdu.Error.Code)
			err = c.tsm.Send(int(apdu.InvokeId), err)
			if err != nil {
				c.log.Debug("unable to send error to %d: %v", apdu.InvokeId, err)
			}
		default:
			// Ignore it
			//log.WithFields(log.Fields{"raw": b}).Debug("An ignored packet went through")
		}
	}

	if header.Function == bactype.BacFuncForwardedNPDU {
		// Right now we are ignoring the NPDU data that is stored in the packet. Eventually
		// we will need to check it for any additional information we can gleam.
		// NDPU has source
		b = b[forwardHeaderLength:]
		c.log.Debug("Ignored NDPU Forwarded")
	}

}
  • 发送请求时,源代码的报文是包含源地址的,但是与scan的请求报文不一致,所以请求报文不填源地址。比如whois中npdu里的source直接注释掉,这样发出去请求就能返回设备信息,读属性和读多属性也是,有source就不行,没有就能成功读到,不知道为啥。
func (c *Client) WhoIs(low, high int) ([]types.Device, error) {
	dest := types.UDPToAddress(&net.UDPAddr{
		IP:   c.broadcastAddress,
		Port: DefaultPort,
	})
	// src, _ := c.localAddress()
	// ip, _, _ := net.ParseCIDR(c.myAddress)
	// src := types.UDPToAddress(&net.UDPAddr{
	// 	IP:   ip,
	// 	Port: c.port,
	// })

	dest.SetBroadcast(true)

	enc := encoding.NewEncoder()
	npdu := types.NPDU{
		Version:     types.ProtocolVersion,
		Destination: &dest,
		// Source:                &src,
		IsNetworkLayerMessage: false,

		// We are not expecting a direct reply from a single destination
		ExpectingReply: false,
		Priority:       types.Normal,
		HopCount:       types.DefaultHopCount,
	}
	enc.NPDU(npdu)

	err := enc.WhoIs(int32(low), int32(high))
	if err != nil {
		return nil, err
	}

	// Subscribe to any changes in the the range. If it is a broadcast,
	var start, end int
	if low == -1 || high == -1 {
		start = 0
		end = maxInt
	} else {
		start = low
		end = high
	}

	// Run in parallel
	errChan := make(chan error)
	go func() {
		_, err = c.send(dest, enc.Bytes())
		errChan <- err
	}()
	values, err := c.utsm.Subscribe(start, end)
	if err != nil {
		return nil, err
	}
	err = <-errChan
	if err != nil {
		return nil, err
	}

	// Weed out values that are not important such as non object type
	// and that are not
	uniqueMap := make(map[types.ObjectInstance]types.Device)
	uniqueList := make([]types.Device, len(uniqueMap))
	for _, v := range values {
		r, ok := v.(types.IAm)

		// Skip non I AM responses
		if !ok {
			continue
		}
		//声明一个地址
		// addrsrc := types.Address{}
		// addrsrc = r.Addr
		// addrsrc.Mac = dest.Mac
		// addrsrc.MacLen = uint8(len(dest.Mac))
		// Check to see if we are in the map before inserting
		if _, ok := uniqueMap[r.ID.Instance]; !ok {
			dev := types.Device{
				Addr:         r.Addr,
				ID:           r.ID,
				MaxApdu:      r.MaxApdu,
				Segmentation: r.Segmentation,
				Vendor:       r.Vendor,
			}
			uniqueMap[r.ID.Instance] = types.Device(dev)
			uniqueList = append(uniqueList, dev)
		}
	}
	return uniqueList, err
}
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值