From 42eb4439990f2b2f8e5a173e7d44b2314b637f53 Mon Sep 17 00:00:00 2001 From: Imre Halasz Date: Thu, 25 Jun 2026 11:14:08 +0200 Subject: [PATCH 1/4] ns: Add missing MAC settings profile ID path fields --- pkg/networkserver/grpc_deviceregistry.go | 1 + pkg/networkserver/grpc_gsns.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/networkserver/grpc_deviceregistry.go b/pkg/networkserver/grpc_deviceregistry.go index 5717091a27..dc2223a63c 100644 --- a/pkg/networkserver/grpc_deviceregistry.go +++ b/pkg/networkserver/grpc_deviceregistry.go @@ -1533,6 +1533,7 @@ func (ns *NetworkServer) ResetFactoryDefaults(ctx context.Context, req *ttnpb.Re "lorawan_phy_version", "lorawan_version", "mac_settings", + "mac_settings_profile_ids", "multicast", "session.dev_addr", "session.keys", diff --git a/pkg/networkserver/grpc_gsns.go b/pkg/networkserver/grpc_gsns.go index 604586e5de..09b1604780 100644 --- a/pkg/networkserver/grpc_gsns.go +++ b/pkg/networkserver/grpc_gsns.go @@ -840,11 +840,13 @@ func appendRecentUplink( } var handleDataUplinkGetPaths = [...]string{ + "battery_percentage", "frequency_plan_id", "last_dev_status_received_at", "lorawan_phy_version", "lorawan_version", "mac_settings", + "mac_settings_profile_ids", "mac_state", "multicast", "pending_mac_state", @@ -853,7 +855,6 @@ var handleDataUplinkGetPaths = [...]string{ "supports_class_b", "supports_class_c", "supports_join", - "battery_percentage", } // mergeMetadata merges the metadata collected for up. @@ -1264,6 +1265,7 @@ func (ns *NetworkServer) handleJoinRequest(ctx context.Context, up *ttnpb.Uplink "lorawan_phy_version", "lorawan_version", "mac_settings", + "mac_settings_profile_ids", "session.dev_addr", "supports_class_b", "supports_class_c", From 6885565e9f3c27ba28f6ce58e8e31625f4e53ac5 Mon Sep 17 00:00:00 2001 From: Imre Halasz Date: Thu, 25 Jun 2026 13:29:24 +0200 Subject: [PATCH 2/4] dev: Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 048a7ef8a9..1f3b4e5cc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ For details about compatibility between different releases, see the **Commitment ### Fixed +- Applying the MAC settings profile values to the device's MAC state during the join procedure (OTAA) or factory reset (ABP). + ### Security ## [3.36.1] - 2026-06-24 From 621ed7670a2ccdba56ed69f72c5d054d809583d6 Mon Sep 17 00:00:00 2001 From: Imre Halasz Date: Mon, 29 Jun 2026 15:34:48 +0200 Subject: [PATCH 3/4] ns: Add factory reset test for ABP devices with MAC settings profile --- pkg/networkserver/grpc_deviceregistry_test.go | 66 ++++++++++++++----- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/pkg/networkserver/grpc_deviceregistry_test.go b/pkg/networkserver/grpc_deviceregistry_test.go index 3aa36c2e4b..646461ad4c 100644 --- a/pkg/networkserver/grpc_deviceregistry_test.go +++ b/pkg/networkserver/grpc_deviceregistry_test.go @@ -1019,9 +1019,23 @@ func TestDeviceRegistryResetFactoryDefaults(t *testing.T) { macSettings := test.Must(DefaultConfig.DefaultMACSettings.Parse()) activateOpt := EndDeviceOptions.Activate(macSettings, true, activeSessionOpts) + macSettingsProfileID := &ttnpb.MACSettingsProfileIdentifiers{ + ApplicationIds: &ttnpb.ApplicationIdentifiers{ + ApplicationId: "test-app-id", + }, + ProfileId: "test-mac-settings-profile-id", + } + macSettingsProfile := &ttnpb.MACSettingsProfile{ + Ids: macSettingsProfileID, + MacSettings: &ttnpb.MACSettings{ + Rx1Delay: &ttnpb.RxDelayValue{Value: ttnpb.RxDelay_RX_DELAY_2}, + }, + } + // TODO: Refactor into same structure as Set for _, tc := range []struct { CreateDevice *SetDeviceRequest + Profile *ttnpb.MACSettingsProfile }{ {}, @@ -1059,6 +1073,12 @@ func TestDeviceRegistryResetFactoryDefaults(t *testing.T) { EndDeviceOptions.WithLorawanPhyVersion(ttnpb.PHYVersion_RP001_V1_0_3_REV_A), }), }, + { + CreateDevice: MakeABPSetDeviceRequest(macSettings, activeSessionOpts, nil, []test.EndDeviceOption{ + EndDeviceOptions.WithMacSettingsProfileIds(macSettingsProfileID), + }), + Profile: macSettingsProfile, + }, } { for _, conf := range []struct { Paths []string @@ -1134,22 +1154,24 @@ func TestDeviceRegistryResetFactoryDefaults(t *testing.T) { if tc.CreateDevice == nil { return "no device" } - return MakeTestCaseName( - fmt.Sprintf("paths:[%s]", strings.Join(conf.Paths, ",")), - func() string { - if tc.CreateDevice.EndDevice.SupportsJoin { - return "OTAA" - } - if tc.CreateDevice.EndDevice.Session == nil { - return MakeTestCaseName("ABP", "no session") - } - return fmt.Sprintf(MakeTestCaseName("ABP", "dev_addr:%s", "queue_len:%d", "session_keys:%v"), - types.MustDevAddr(tc.CreateDevice.Session.DevAddr).OrZero(), - len(tc.CreateDevice.EndDevice.Session.QueuedApplicationDownlinks), - tc.CreateDevice.Session.Keys, - ) - }(), - ) + nameParts := []string{fmt.Sprintf("paths:[%s]", strings.Join(conf.Paths, ","))} + switch { + case tc.CreateDevice.SupportsJoin: + nameParts = append(nameParts, "OTAA") + case tc.CreateDevice.Session == nil: + nameParts = append(nameParts, MakeTestCaseName("ABP", "no session")) + default: + nameParts = append(nameParts, fmt.Sprintf( + MakeTestCaseName("ABP", "dev_addr:%s", "queue_len:%d", "session_keys:%v"), + types.MustDevAddr(tc.CreateDevice.Session.DevAddr).OrZero(), + len(tc.CreateDevice.Session.QueuedApplicationDownlinks), + tc.CreateDevice.Session.Keys, + )) + if tc.Profile != nil { + nameParts = append(nameParts, "with_profile") + } + } + return MakeTestCaseName(nameParts...) }(), Parallel: true, Func: func(ctx context.Context, t *testing.T, a *assertions.Assertion) { @@ -1183,6 +1205,16 @@ func TestDeviceRegistryResetFactoryDefaults(t *testing.T) { clock := test.NewMockClock(time.Now().UTC()) defer SetMockClock(clock)() + if tc.Profile != nil { + _, err := env.MACSettingsProfileRegistry.Set(ctx, tc.Profile.Ids, []string{"ids", "mac_settings"}, + func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error) { + return tc.Profile, []string{"ids", "mac_settings"}, nil + }) + if !a.So(err, should.BeNil) { + return + } + } + req := &ttnpb.ResetAndGetEndDeviceRequest{ EndDeviceIds: test.MakeEndDeviceIdentifiers(), FieldMask: ttnpb.FieldMask(conf.Paths...), @@ -1244,7 +1276,7 @@ func TestDeviceRegistryResetFactoryDefaults(t *testing.T) { } var newErr error defaultMACSettings := test.Must(DefaultConfig.DefaultMACSettings.Parse()) - macState, newErr = mac.NewState(created, fps, defaultMACSettings, nil) + macState, newErr = mac.NewState(created, fps, defaultMACSettings, tc.Profile.GetMacSettings()) if newErr != nil { a.So(err, should.NotBeNil) a.So(err, should.HaveSameErrorDefinitionAs, newErr) From a65fb3e0714ada7aec52645d0b1ffdf3f6673fac Mon Sep 17 00:00:00 2001 From: Imre Halasz Date: Mon, 29 Jun 2026 17:01:22 +0200 Subject: [PATCH 4/4] ns: Add flow test for OTAA device with MAC settings profile --- pkg/networkserver/networkserver_flow_test.go | 56 +++++++++++++++++++ .../networkserver_util_internal_test.go | 4 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/pkg/networkserver/networkserver_flow_test.go b/pkg/networkserver/networkserver_flow_test.go index cd9cc6de23..9cbfbc242a 100644 --- a/pkg/networkserver/networkserver_flow_test.go +++ b/pkg/networkserver/networkserver_flow_test.go @@ -157,6 +157,8 @@ type OTAAFlowTestConfig struct { CreateDevice *ttnpb.SetEndDeviceRequest Func func(context.Context, TestEnvironment, *ttnpb.EndDevice) + Profile *ttnpb.MACSettingsProfile + UplinkMACCommanders []MACCommander UplinkEventBuilders []events.Builder DownlinkHeadMACCommanders []MACCommander @@ -171,6 +173,18 @@ func makeOTAAFlowTest(conf OTAAFlowTestConfig) func(context.Context, TestEnviron start := time.Now() + if conf.Profile != nil { + profilePaths := []string{"ids", "mac_settings"} // nolint: goconst + _, err := env.MACSettingsProfileRegistry.Set(ctx, conf.Profile.Ids, profilePaths, + func(context.Context, *ttnpb.MACSettingsProfile) (*ttnpb.MACSettingsProfile, []string, error) { + return conf.Profile, profilePaths, nil + }) + if !a.So(err, should.BeNil) { + t.Error("Failed to register MAC settings profile") + return + } + } + dev, err, ok := env.AssertSetDevice(ctx, true, conf.CreateDevice, ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, ) @@ -202,6 +216,8 @@ func makeOTAAFlowTest(conf OTAAFlowTestConfig) func(context.Context, TestEnviron "GsNs-join-2", }, + Profile: conf.Profile, + ClusterResponse: &NsJsHandleJoinResponse{ Response: &ttnpb.JoinResponse{ RawPayload: bytes.Repeat([]byte{0x42}, responseLen), @@ -432,11 +448,51 @@ func makeClassCOTAAFlowTest(macVersion ttnpb.MACVersion, phyVersion ttnpb.PHYVer }) } +func makeOTAAFlowTestWithMACSettingsProfile( // nolint: lll + macVersion ttnpb.MACVersion, phyVersion ttnpb.PHYVersion, fpID string, +) func(context.Context, TestEnvironment) { + const testProfileID = "test-mac-settings-profile-id" + profileID := &ttnpb.MACSettingsProfileIdentifiers{ + ApplicationIds: test.DefaultApplicationIdentifiers, + ProfileId: testProfileID, + } + profile := &ttnpb.MACSettingsProfile{ + Ids: profileID, + MacSettings: &ttnpb.MACSettings{ + Rx1Delay: &ttnpb.RxDelayValue{Value: ttnpb.RxDelay_RX_DELAY_2}, + }, + } + return makeOTAAFlowTest(OTAAFlowTestConfig{ + Profile: profile, + CreateDevice: &ttnpb.SetEndDeviceRequest{ + EndDevice: MakeOTAAEndDevice( + EndDeviceOptions.WithFrequencyPlanId(fpID), + EndDeviceOptions.WithLorawanVersion(macVersion), + EndDeviceOptions.WithLorawanPhyVersion(phyVersion), + EndDeviceOptions.WithMacSettingsProfileIds(profileID), + ), + FieldMask: ttnpb.FieldMask( + "frequency_plan_id", + "lorawan_phy_version", + "lorawan_version", + "mac_settings_profile_ids", + "supports_join", + ), + }, + DownlinkTailMACCommanders: []MACCommander{ttnpb.MACCommandIdentifier_CID_DEV_STATUS}, + DownlinkTailEventBuilders: []events.Builder{mac.EvtEnqueueDevStatusRequest}, + Func: func(context.Context, TestEnvironment, *ttnpb.EndDevice) {}, + }) +} + func TestFlow(t *testing.T) { ForEachFrequencyPlanLoRaWANVersionPair(t, func(makeName func(...string) string, fpID string, _ *frequencyplans.FrequencyPlan, phy *band.Band, macVersion ttnpb.MACVersion, phyVersion ttnpb.PHYVersion) { for flowName, handleFlowTest := range map[string]func(context.Context, TestEnvironment){ MakeTestCaseName("Class A", "OTAA"): makeClassAOTAAFlowTest(macVersion, phyVersion, fpID), MakeTestCaseName("Class C", "OTAA"): makeClassCOTAAFlowTest(macVersion, phyVersion, fpID), + MakeTestCaseName("Class A", "OTAA", "with profile"): makeOTAAFlowTestWithMACSettingsProfile( // nolint: lll + macVersion, phyVersion, fpID, + ), } { test.RunSubtest(t, test.SubtestConfig{ Name: makeName(flowName), diff --git a/pkg/networkserver/networkserver_util_internal_test.go b/pkg/networkserver/networkserver_util_internal_test.go index e511810c76..86fda756aa 100644 --- a/pkg/networkserver/networkserver_util_internal_test.go +++ b/pkg/networkserver/networkserver_util_internal_test.go @@ -1560,6 +1560,8 @@ type JoinAssertionConfig struct { RxMetadatas [][]*ttnpb.RxMetadata CorrelationIDs []string + Profile *ttnpb.MACSettingsProfile + ClusterResponse *NsJsHandleJoinResponse InteropResponse *InteropClientHandleJoinRequestResponse } @@ -1600,7 +1602,7 @@ func (env TestEnvironment) AssertJoin(ctx context.Context, conf JoinAssertionCon t, a := test.MustNewTFromContext(ctx) t.Helper() - var profileMACSettings *ttnpb.MACSettings + profileMACSettings := conf.Profile.GetMacSettings() defaultMACSettings := test.Must(env.Config.DefaultMACSettings.Parse()) defaultLoRaWANVersion := mac.DeviceDefaultLoRaWANVersion(conf.Device)