Example #1
0
// Update updates the node matching the given DevEUI.
func (a *NodeAPI) Update(ctx context.Context, req *pb.UpdateNodeRequest) (*pb.UpdateNodeResponse, error) {
	var appEUI, devEUI lorawan.EUI64
	var appKey lorawan.AES128Key

	if err := appEUI.UnmarshalText([]byte(req.AppEUI)); err != nil {
		return nil, err
	}
	if err := devEUI.UnmarshalText([]byte(req.DevEUI)); err != nil {
		return nil, err
	}
	if err := appKey.UnmarshalText([]byte(req.AppKey)); err != nil {
		return nil, err
	}

	node, err := storage.GetNode(a.ctx.DB, devEUI)
	if err != nil {
		return nil, err
	}

	node.AppEUI = appEUI
	node.AppKey = appKey
	node.RXDelay = uint8(req.RxDelay)
	node.RX1DROffset = uint8(req.Rx1DROffset)
	if req.ChannelListID > 0 {
		node.ChannelListID = &req.ChannelListID
	} else {
		node.ChannelListID = nil
	}

	if err := storage.UpdateNode(a.ctx.DB, node); err != nil {
		return nil, err
	}

	return &pb.UpdateNodeResponse{}, nil
}
// validateAndCollectJoinRequestPacket validates and collects a single
// received RXPacket of type join-request.
func validateAndCollectJoinRequestPacket(ctx Context, rxPacket models.RXPacket) error {
	// MACPayload must be of type *lorawan.JoinRequestPayload
	jrPL, ok := rxPacket.PHYPayload.MACPayload.(*lorawan.JoinRequestPayload)
	if !ok {
		return fmt.Errorf("expected *lorawan.JoinRequestPayload, got: %T", rxPacket.PHYPayload.MACPayload)
	}

	// get node information for this DevEUI
	node, err := storage.GetNode(ctx.DB, jrPL.DevEUI)
	if err != nil {
		return err
	}

	// validate that the DevEUI belongs to the given AppEUI
	if node.AppEUI != jrPL.AppEUI {
		return fmt.Errorf("node %s belongs to application %s, %s was given", jrPL.DevEUI, node.AppEUI, jrPL.AppEUI)
	}

	// validate the MIC
	ok, err = rxPacket.PHYPayload.ValidateMIC(node.AppKey)
	if err != nil {
		return fmt.Errorf("validate MIC error: %s", err)
	}
	if !ok {
		return errors.New("invalid MIC")
	}

	return collectAndCallOnce(ctx.RedisPool, rxPacket, func(rxPackets RXPackets) error {
		return handleCollectedJoinRequestPackets(ctx, rxPackets)
	})

}
Example #3
0
func (a *NodeSessionAPI) validateNodeSession(ns models.NodeSession) error {
	// validate the NwkID
	if ns.DevAddr.NwkID() != a.ctx.NetID.NwkID() {
		return fmt.Errorf("DevAddr must contain NwkID %s", hex.EncodeToString([]byte{a.ctx.NetID.NwkID()}))
	}

	// validate that the node exists
	var node models.Node
	var err error
	if node, err = storage.GetNode(a.ctx.DB, ns.DevEUI); err != nil {
		return err
	}

	// validate that the node belongs to the given AppEUI.
	if ns.AppEUI != node.AppEUI {
		return fmt.Errorf("DevEUI %s belongs to AppEUI %s, got AppEUI %s", ns.AppEUI, node.AppEUI, ns.AppEUI)
	}

	return nil
}
Example #4
0
// Get returns the Node for the given DevEUI.
func (a *NodeAPI) Get(ctx context.Context, req *pb.GetNodeRequest) (*pb.GetNodeResponse, error) {
	var eui lorawan.EUI64
	if err := eui.UnmarshalText([]byte(req.DevEUI)); err != nil {
		return nil, err
	}

	node, err := storage.GetNode(a.ctx.DB, eui)
	if err != nil {
		return nil, err
	}

	devEUI, err := node.DevEUI.MarshalText()
	if err != nil {
		return nil, err
	}
	appEUI, err := node.AppEUI.MarshalText()
	if err != nil {
		return nil, err
	}
	appKey, err := node.AppKey.MarshalText()
	if err != nil {
		return nil, err
	}

	resp := pb.GetNodeResponse{
		DevEUI:      string(devEUI),
		AppEUI:      string(appEUI),
		AppKey:      string(appKey),
		RxDelay:     uint32(node.RXDelay),
		Rx1DROffset: uint32(node.RX1DROffset),
	}

	if node.ChannelListID != nil {
		resp.ChannelListID = *node.ChannelListID
	}

	return &resp, nil
}
func TestHandleJoinRequestPackets(t *testing.T) {
	conf := common.GetTestConfig()

	Convey("Given a dummy gateway and application backend and a clean Postgres and Redis database", t, func() {
		a := &testApplicationBackend{
			rxPayloadChan:           make(chan models.RXPayload, 1),
			notificationPayloadChan: make(chan interface{}, 10),
		}
		g := &testGatewayBackend{
			rxPacketChan: make(chan models.RXPacket, 1),
			txPacketChan: make(chan models.TXPacket, 1),
		}
		p := storage.NewRedisPool(conf.RedisURL)
		common.MustFlushRedis(p)
		db, err := storage.OpenDatabase(conf.PostgresDSN)
		So(err, ShouldBeNil)
		common.MustResetDB(db)

		ctx := Context{
			RedisPool:   p,
			Gateway:     g,
			Application: a,
			DB:          db,
		}

		Convey("Given a node and application in the database", func() {
			app := models.Application{
				AppEUI: [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
				Name:   "test app",
			}
			So(storage.CreateApplication(ctx.DB, app), ShouldBeNil)

			node := models.Node{
				DevEUI: [8]byte{8, 7, 6, 5, 4, 3, 2, 1},
				AppEUI: app.AppEUI,
				AppKey: [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},

				RXDelay:     3,
				RX1DROffset: 2,
			}
			So(storage.CreateNode(ctx.DB, node), ShouldBeNil)

			Convey("Given a JoinRequest with correct DevEUI but incorrect AppEUI", func() {
				phy := lorawan.PHYPayload{
					MHDR: lorawan.MHDR{
						MType: lorawan.JoinRequest,
						Major: lorawan.LoRaWANR1,
					},
					MACPayload: &lorawan.JoinRequestPayload{
						AppEUI:   [8]byte{1, 2, 3, 4, 5, 6, 7, 9},
						DevEUI:   node.DevEUI,
						DevNonce: [2]byte{1, 2},
					},
				}
				So(phy.SetMIC(node.AppKey), ShouldBeNil)

				rxPacket := models.RXPacket{
					PHYPayload: phy,
					RXInfo: models.RXInfo{
						Frequency: common.Band.UplinkChannels[0].Frequency,
						DataRate:  common.Band.DataRates[common.Band.UplinkChannels[0].DataRates[0]],
					},
				}

				Convey("then handleRXPacket returns an error", func() {
					So(handleRXPacket(ctx, rxPacket), ShouldResemble, errors.New("node 0807060504030201 belongs to application 0102030405060708, 0102030405060709 was given"))
				})
			})

			Convey("Given a JoinRequest packet", func() {
				phy := lorawan.PHYPayload{
					MHDR: lorawan.MHDR{
						MType: lorawan.JoinRequest,
						Major: lorawan.LoRaWANR1,
					},
					MACPayload: &lorawan.JoinRequestPayload{
						AppEUI:   app.AppEUI,
						DevEUI:   node.DevEUI,
						DevNonce: [2]byte{1, 2},
					},
				}
				So(phy.SetMIC(node.AppKey), ShouldBeNil)

				rxPacket := models.RXPacket{
					PHYPayload: phy,
					RXInfo: models.RXInfo{
						Frequency: common.Band.UplinkChannels[0].Frequency,
						DataRate:  common.Band.DataRates[common.Band.UplinkChannels[0].DataRates[0]],
					},
				}

				Convey("When calling handleRXPacket", func() {
					So(handleRXPacket(ctx, rxPacket), ShouldBeNil)

					Convey("Then a JoinAccept was sent to the node", func() {
						txPacket := <-g.txPacketChan
						phy := txPacket.PHYPayload
						So(phy.DecryptJoinAcceptPayload(node.AppKey), ShouldBeNil)
						So(phy.MHDR.MType, ShouldEqual, lorawan.JoinAccept)

						Convey("Then it was sent after 5s", func() {
							So(txPacket.TXInfo.Timestamp, ShouldEqual, rxPacket.RXInfo.Timestamp+uint32(5*time.Second/time.Microsecond))
						})

						Convey("Then the RXDelay is set to 3s", func() {
							jaPL := phy.MACPayload.(*lorawan.JoinAcceptPayload)
							So(jaPL.RXDelay, ShouldEqual, 3)
						})

						Convey("Then the DLSettings are set correctly", func() {
							jaPL := phy.MACPayload.(*lorawan.JoinAcceptPayload)
							So(jaPL.DLSettings.RX2DataRate, ShouldEqual, uint8(common.Band.RX2DataRate))
							So(jaPL.DLSettings.RX1DROffset, ShouldEqual, node.RX1DROffset)
						})

						Convey("Then a node-session was created", func() {
							jaPL := phy.MACPayload.(*lorawan.JoinAcceptPayload)

							_, err := storage.GetNodeSession(ctx.RedisPool, jaPL.DevAddr)
							So(err, ShouldBeNil)
						})

						Convey("Then the dev-nonce was added to the used dev-nonces", func() {
							node, err := storage.GetNode(ctx.DB, node.DevEUI)
							So(err, ShouldBeNil)
							So([2]byte{1, 2}, ShouldBeIn, node.UsedDevNonces)
						})

						Convey("Then a join notification was sent to the application", func() {
							notification := <-a.notificationPayloadChan
							join, ok := notification.(models.JoinNotification)
							So(ok, ShouldBeTrue)
							So(join.DevEUI, ShouldResemble, node.DevEUI)
						})
					})
				})
			})
		})
	})
}
// handleCollectedJoinRequestPackets handles the received join-request.
func handleCollectedJoinRequestPackets(ctx Context, rxPackets RXPackets) error {
	if len(rxPackets) == 0 {
		return errors.New("packet collector returned 0 packets")
	}
	rxPacket := rxPackets[0]

	var macs []string
	for _, p := range rxPackets {
		macs = append(macs, p.RXInfo.MAC.String())
	}

	// MACPayload must be of type *lorawan.JoinRequestPayload
	jrPL, ok := rxPacket.PHYPayload.MACPayload.(*lorawan.JoinRequestPayload)
	if !ok {
		return fmt.Errorf("expected *lorawan.JoinRequestPayload, got: %T", rxPacket.PHYPayload.MACPayload)
	}

	log.WithFields(log.Fields{
		"dev_eui":  jrPL.DevEUI,
		"gw_count": len(rxPackets),
		"gw_macs":  strings.Join(macs, ", "),
		"mtype":    rxPackets[0].PHYPayload.MHDR.MType,
	}).Info("packet(s) collected")

	// get node information for this DevEUI
	node, err := storage.GetNode(ctx.DB, jrPL.DevEUI)
	if err != nil {
		return err
	}

	// validate the given nonce
	if !node.ValidateDevNonce(jrPL.DevNonce) {
		return fmt.Errorf("given dev-nonce %x has already been used before for node %s", jrPL.DevNonce, jrPL.DevEUI)
	}

	// get random (free) DevAddr
	devAddr, err := storage.GetRandomDevAddr(ctx.RedisPool, ctx.NetID)
	if err != nil {
		return fmt.Errorf("get random DevAddr error: %s", err)
	}

	// get app nonce
	appNonce, err := getAppNonce()
	if err != nil {
		return fmt.Errorf("get AppNonce error: %s", err)
	}

	// get the (optional) CFList
	cFList, err := storage.GetCFListForNode(ctx.DB, node)
	if err != nil {
		return fmt.Errorf("get CFList for node error: %s", err)
	}

	// get keys
	nwkSKey, err := getNwkSKey(node.AppKey, ctx.NetID, appNonce, jrPL.DevNonce)
	if err != nil {
		return fmt.Errorf("get NwkSKey error: %s", err)
	}
	appSKey, err := getAppSKey(node.AppKey, ctx.NetID, appNonce, jrPL.DevNonce)
	if err != nil {
		return fmt.Errorf("get AppSKey error: %s", err)
	}

	ns := models.NodeSession{
		DevAddr:  devAddr,
		DevEUI:   jrPL.DevEUI,
		AppSKey:  appSKey,
		NwkSKey:  nwkSKey,
		FCntUp:   0,
		FCntDown: 0,

		AppEUI:      node.AppEUI,
		RXDelay:     node.RXDelay,
		RX1DROffset: node.RX1DROffset,
		CFList:      cFList,
	}
	if err = storage.SaveNodeSession(ctx.RedisPool, ns); err != nil {
		return fmt.Errorf("save node-session error: %s", err)
	}

	// update the node (with updated used dev-nonces)
	if err = storage.UpdateNode(ctx.DB, node); err != nil {
		return fmt.Errorf("update node error: %s", err)
	}

	// construct the lorawan packet
	phy := lorawan.PHYPayload{
		MHDR: lorawan.MHDR{
			MType: lorawan.JoinAccept,
			Major: lorawan.LoRaWANR1,
		},
		MACPayload: &lorawan.JoinAcceptPayload{
			AppNonce: appNonce,
			NetID:    ctx.NetID,
			DevAddr:  ns.DevAddr,
			RXDelay:  ns.RXDelay,
			DLSettings: lorawan.DLSettings{
				RX2DataRate: uint8(common.Band.RX2DataRate),
				RX1DROffset: ns.RX1DROffset,
			},
			CFList: cFList,
		},
	}
	if err = phy.SetMIC(node.AppKey); err != nil {
		return fmt.Errorf("set MIC error: %s", err)
	}
	if err = phy.EncryptJoinAcceptPayload(node.AppKey); err != nil {
		return fmt.Errorf("encrypt join-accept error: %s", err)
	}

	// get data-rate
	uplinkDR, err := common.Band.GetDataRate(rxPacket.RXInfo.DataRate)
	if err != nil {
		return err
	}
	rx1DR := common.Band.RX1DataRate[uplinkDR][0]

	// get frequency
	rx1Freq, err := common.Band.GetRX1Frequency(rxPacket.RXInfo.Frequency)
	if err != nil {
		return err
	}

	txPacket := models.TXPacket{
		TXInfo: models.TXInfo{
			MAC:       rxPacket.RXInfo.MAC,
			Timestamp: rxPacket.RXInfo.Timestamp + uint32(common.Band.JoinAcceptDelay1/time.Microsecond),
			Frequency: rx1Freq,
			Power:     common.Band.DefaultTXPower,
			DataRate:  common.Band.DataRates[rx1DR],
			CodeRate:  rxPacket.RXInfo.CodeRate,
		},
		PHYPayload: phy,
	}

	// window 1
	if err = ctx.Gateway.SendTXPacket(txPacket); err != nil {
		return fmt.Errorf("send tx packet (rx window 1) to gateway error: %s", err)
	}

	// send a notification to the application that a node joined the network
	return ctx.Application.SendNotification(ns.AppEUI, ns.DevEUI, models.JoinNotificationType, models.JoinNotification{
		DevAddr: ns.DevAddr,
		DevEUI:  ns.DevEUI,
	})
}