From 2042893b81da3894ed9f472472122d4359257367 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Fri, 16 Jan 2026 20:58:20 +0200 Subject: [PATCH 01/10] dcs: Implement the Update method in GatewayRegistry --- .../registry/gateways/gateways.go | 15 +++++++++++++++ pkg/deviceclaimingserver/util_test.go | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/pkg/deviceclaimingserver/registry/gateways/gateways.go b/pkg/deviceclaimingserver/registry/gateways/gateways.go index 419bb44409..61d79bd35d 100644 --- a/pkg/deviceclaimingserver/registry/gateways/gateways.go +++ b/pkg/deviceclaimingserver/registry/gateways/gateways.go @@ -47,6 +47,8 @@ type GatewayRegistry interface { Delete(ctx context.Context, in *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) // Get the gateway. This may not release the gateway ID for reuse, but it does release the EUI. Get(ctx context.Context, req *ttnpb.GetGatewayRequest) (*ttnpb.Gateway, error) + // Update the gateway. + Update(ctx context.Context, req *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) } // Registry implements GatewayRegistry. @@ -129,3 +131,16 @@ func (reg Registry) Get(ctx context.Context, req *ttnpb.GetGatewayRequest) (*ttn } return gatewayRegistry.Get(ctx, req, callOpt) } + +// Update implements GatewayRegistry. +func (reg Registry) Update(ctx context.Context, req *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { + callOpt, err := reg.callOptFromContext(ctx) + if err != nil { + return nil, err + } + gatewayRegistry, err := reg.newEntityRegistryClient(ctx) + if err != nil { + return nil, err + } + return gatewayRegistry.Update(ctx, req, callOpt) +} diff --git a/pkg/deviceclaimingserver/util_test.go b/pkg/deviceclaimingserver/util_test.go index 102190da55..43bdf6ed33 100644 --- a/pkg/deviceclaimingserver/util_test.go +++ b/pkg/deviceclaimingserver/util_test.go @@ -113,6 +113,7 @@ type mockGatewayRegistry struct { createFunc func(ctx context.Context, in *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) deleteFunc func(ctx context.Context, in *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) getFunc func(ctx context.Context, req *ttnpb.GetGatewayRequest) (*ttnpb.Gateway, error) + updateFunc func(ctx context.Context, req *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) } var ( @@ -163,3 +164,8 @@ func (mock mockGatewayRegistry) Delete(ctx context.Context, in *ttnpb.GatewayIde func (mock mockGatewayRegistry) Get(ctx context.Context, in *ttnpb.GetGatewayRequest) (*ttnpb.Gateway, error) { return mock.getFunc(ctx, in) } + +// Update implements GatewayRegistry. +func (mock mockGatewayRegistry) Update(ctx context.Context, req *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { + return mock.updateFunc(ctx, req) +} From 22a5612d76a0ed3040ce3b9fe2f524bc2e7aafa2 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:03:07 +0200 Subject: [PATCH 02/10] dcs: Create gateway at the start of the Claim to attach keys to gateway --- pkg/deviceclaimingserver/grpc_gateways.go | 50 +++++++++++-- .../grpc_gateways_test.go | 70 +++++++++++++++++-- pkg/deviceclaimingserver/types/types.go | 3 +- pkg/deviceclaimingserver/util_test.go | 1 + 4 files changed, 111 insertions(+), 13 deletions(-) diff --git a/pkg/deviceclaimingserver/grpc_gateways.go b/pkg/deviceclaimingserver/grpc_gateways.go index 3b93150ee8..35ad56fb29 100644 --- a/pkg/deviceclaimingserver/grpc_gateways.go +++ b/pkg/deviceclaimingserver/grpc_gateways.go @@ -27,6 +27,7 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) type peerAccess interface { @@ -107,6 +108,27 @@ func (gcls *gatewayClaimingServer) Claim( return nil, err } + // Create the gateway in the IS. + gateway := &ttnpb.Gateway{ + Ids: ids, + } + + _, err = gcls.registry.Create(ctx, &ttnpb.CreateGatewayRequest{ + Gateway: gateway, + Collaborator: req.GetCollaborator(), + }) + if err != nil { + return nil, errCreateGateway.WithCause(err) + } + defer func() { + if retErr != nil { + logger.Warn("Failed to claim gateway, deleting created gateway") + if _, delErr := gcls.registry.Delete(ctx, ids); delErr != nil { + logger.WithError(delErr).Warn("Failed to delete created gateway after failed claim") + } + } + }() + // Support clients that only set a single frequency plan. if len(req.TargetFrequencyPlanIds) == 0 && req.TargetFrequencyPlanId != "" { // nolint:staticcheck req.TargetFrequencyPlanIds = []string{req.TargetFrequencyPlanId} // nolint:staticcheck @@ -125,7 +147,7 @@ func (gcls *gatewayClaimingServer) Claim( return nil, errClaim.WithCause(err) } - // Unclaim if creation fails. + // Unclaim if update fails. defer func(ids *ttnpb.GatewayIdentifiers) { if retErr != nil { observability.RegisterAbortClaim(ctx, ids.GetEntityIdentifiers(), retErr) @@ -137,8 +159,9 @@ func (gcls *gatewayClaimingServer) Claim( observability.RegisterSuccessClaim(ctx, ids.GetEntityIdentifiers()) }(ids) - // Create the gateway in the IS. - gateway := &ttnpb.Gateway{ + // Update the gateway in the IS. If the update fails, the gateway will be unclaimed in the above deferred function + // and deleted in the previous one. + gateway = &ttnpb.Gateway{ Ids: ids, GatewayServerAddress: req.TargetGatewayServerAddress, EnforceDutyCycle: true, @@ -147,9 +170,24 @@ func (gcls *gatewayClaimingServer) Claim( Antennas: res.Antennas, } - _, err = gcls.registry.Create(ctx, &ttnpb.CreateGatewayRequest{ - Gateway: gateway, - Collaborator: req.GetCollaborator(), + fieldMask := &fieldmaskpb.FieldMask{ + Paths: []string{ + "gateway_server_address", + "enforce_duty_cycle", + "require_authenticated_connection", + "frequency_plan_ids", + "antennas", + }, + } + + if res.LBSLNSKey != nil { + gateway.LbsLnsSecret = &ttnpb.Secret{Value: []byte(res.LBSLNSKey.Key)} + fieldMask.Paths = append(fieldMask.Paths, "lbs_lns_secret") + } + + _, err = gcls.registry.Update(ctx, &ttnpb.UpdateGatewayRequest{ + Gateway: gateway, + FieldMask: fieldMask, }) if err != nil { return nil, errCreateGateway.WithCause(err) diff --git a/pkg/deviceclaimingserver/grpc_gateways_test.go b/pkg/deviceclaimingserver/grpc_gateways_test.go index 1e18df9adb..786acbd47a 100644 --- a/pkg/deviceclaimingserver/grpc_gateways_test.go +++ b/pkg/deviceclaimingserver/grpc_gateways_test.go @@ -34,6 +34,7 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/util/test" "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" ) var ( @@ -182,7 +183,9 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest CallOpt grpc.CallOption ClaimFunc func(context.Context, types.EUI64, string, string) (*dcstypes.GatewayMetadata, error) CreateFunc func(context.Context, *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) + UpdateFunc func(context.Context, *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) UnclaimFunc func(context.Context, types.EUI64) error + DeleteFunc func(context.Context, *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) ErrorAssertion func(error) bool }{ { @@ -241,6 +244,25 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest CallOpt: authorizedCallOpt, ErrorAssertion: errors.IsAlreadyExists, }, + { + Name: "Claim/GatewayCreationFailed", + Req: &ttnpb.ClaimGatewayRequest{ + Collaborator: userID.GetOrganizationOrUserIdentifiers(), + SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ + GatewayEui: supportedEUI.Bytes(), + AuthenticationCode: claimAuthCode, + }, + }, + TargetGatewayId: "test-gateway", + TargetGatewayServerAddress: "things.example.com", + }, + CallOpt: authorizedCallOpt, + CreateFunc: func(_ context.Context, in *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { + return nil, errCreate.New() + }, + ErrorAssertion: errors.IsAborted, + }, { Name: "Claim/EUINotRegisteredForClaiming", Req: &ttnpb.ClaimGatewayRequest{ @@ -254,7 +276,13 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest TargetGatewayId: "test-gateway", TargetGatewayServerAddress: "things.example.com", }, - CallOpt: authorizedCallOpt, + CallOpt: authorizedCallOpt, + CreateFunc: func(_ context.Context, in *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { + return in.Gateway, nil + }, + DeleteFunc: func(_ context.Context, _ *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil + }, ErrorAssertion: errors.IsAborted, }, { @@ -271,13 +299,22 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest TargetGatewayServerAddress: "things.example.com", }, CallOpt: authorizedCallOpt, + CreateFunc: func(_ context.Context, in *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { + return in.Gateway, nil + }, ClaimFunc: func(_ context.Context, _ types.EUI64, _, _ string) (*dcstypes.GatewayMetadata, error) { return nil, errClaim.New() }, + UpdateFunc: func(_ context.Context, in *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { + return in.Gateway, nil + }, + DeleteFunc: func(_ context.Context, _ *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil + }, ErrorAssertion: errors.IsAborted, }, { - Name: "Claim/CreateFailed", + Name: "Claim/UpdateFailed", Req: &ttnpb.ClaimGatewayRequest{ Collaborator: userID.GetOrganizationOrUserIdentifiers(), SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ @@ -294,7 +331,13 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest return &dcstypes.GatewayMetadata{}, nil }, CreateFunc: func(context.Context, *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { - return nil, errCreate.New() + return nil, nil + }, + UpdateFunc: func(_ context.Context, in *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { + return nil, errUpdate.New() + }, + DeleteFunc: func(_ context.Context, _ *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil }, UnclaimFunc: func(_ context.Context, eui types.EUI64) error { if eui.Equal(supportedEUI) { @@ -305,7 +348,7 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest ErrorAssertion: errors.IsAborted, }, { - Name: "Claim/CreateFailedWithUnclaimFailed", + Name: "Claim/UpdateFailedWithUnclaimFailed", Req: &ttnpb.ClaimGatewayRequest{ Collaborator: userID.GetOrganizationOrUserIdentifiers(), SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ @@ -322,7 +365,13 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest return &dcstypes.GatewayMetadata{}, nil }, CreateFunc: func(context.Context, *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { - return nil, errCreate.New() + return nil, nil + }, + UpdateFunc: func(_ context.Context, in *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { + return nil, errUpdate.New() + }, + DeleteFunc: func(_ context.Context, _ *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) { + return &emptypb.Empty{}, nil }, UnclaimFunc: func(context.Context, types.EUI64) error { return errUnclaim.New() @@ -330,7 +379,7 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest ErrorAssertion: errors.IsAborted, }, { - Name: "Claim/SuccessfullyClaimedAndCreated", + Name: "Claim/SuccessfullyClaimedAndUpdated", Req: &ttnpb.ClaimGatewayRequest{ Collaborator: userID.GetOrganizationOrUserIdentifiers(), SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ @@ -348,6 +397,9 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest CreateFunc: func(_ context.Context, in *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { return in.Gateway, nil }, + UpdateFunc: func(_ context.Context, in *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { + return in.Gateway, nil + }, CallOpt: authorizedCallOpt, }, } { @@ -361,6 +413,12 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest if tc.CreateFunc != nil { mockGatewayRegistry.createFunc = tc.CreateFunc } + if tc.UpdateFunc != nil { + mockGatewayRegistry.updateFunc = tc.UpdateFunc + } + if tc.DeleteFunc != nil { + mockGatewayRegistry.deleteFunc = tc.DeleteFunc + } _, err := gclsClient.Claim(ctx, tc.Req, tc.CallOpt) if err != nil { diff --git a/pkg/deviceclaimingserver/types/types.go b/pkg/deviceclaimingserver/types/types.go index e27cf94f92..6f68b521f7 100644 --- a/pkg/deviceclaimingserver/types/types.go +++ b/pkg/deviceclaimingserver/types/types.go @@ -62,5 +62,6 @@ func RangeFromEUI64Range(start, end types.EUI64) EUI64Range { // GatewayMetadata contains metadata of a gateway, typically returned on claiming. type GatewayMetadata struct { - Antennas []*ttnpb.GatewayAntenna + Antennas []*ttnpb.GatewayAntenna + LBSLNSKey *ttnpb.APIKey } diff --git a/pkg/deviceclaimingserver/util_test.go b/pkg/deviceclaimingserver/util_test.go index 43bdf6ed33..f5cda85220 100644 --- a/pkg/deviceclaimingserver/util_test.go +++ b/pkg/deviceclaimingserver/util_test.go @@ -121,6 +121,7 @@ var ( errGatewayNotFound = errors.DefineNotFound("gateway_not_found", "gateway not found") errClaim = errors.DefineAborted("claim", "claim") errCreate = errors.DefineAborted("create_gateway", "create gateway") + errUpdate = errors.DefineAborted("update_gateway", "update gateway") errUnclaim = errors.DefineAborted("unclaim", "unclaim gateway") ) From 628982ab07d4177a80b014d865728f2ea6d13a0f Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:46:17 +0200 Subject: [PATCH 03/10] dcs: Implement TTGC LBS CUPS claimer --- pkg/deviceclaimingserver/gateways/gateways.go | 26 +- .../gateways/lbscups/lbscups.go | 320 ++++++++++++++++++ .../gateways/lbscups/root_ca.go | 59 ++++ pkg/ttgc/config.go | 12 +- 4 files changed, 410 insertions(+), 7 deletions(-) create mode 100644 pkg/deviceclaimingserver/gateways/lbscups/lbscups.go create mode 100644 pkg/deviceclaimingserver/gateways/lbscups/root_ca.go diff --git a/pkg/deviceclaimingserver/gateways/gateways.go b/pkg/deviceclaimingserver/gateways/gateways.go index a57ca40f69..e56036b8cd 100644 --- a/pkg/deviceclaimingserver/gateways/gateways.go +++ b/pkg/deviceclaimingserver/gateways/gateways.go @@ -20,12 +20,16 @@ import ( "crypto/tls" "strings" + "go.thethings.network/lorawan-stack/v3/pkg/cluster" "go.thethings.network/lorawan-stack/v3/pkg/config" "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" + "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways/lbscups" "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways/ttgc" dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types" "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" + "google.golang.org/grpc" ) // Component is the interface to the component. @@ -33,6 +37,8 @@ type Component interface { GetBaseConfig(context.Context) config.ServiceBase GetTLSConfig(context.Context) tlsconfig.Config GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error) + GetPeerConn(ctx context.Context, role ttnpb.ClusterRole, ids cluster.EntityIdentifiers) (*grpc.ClientConn, error) + AllowInsecureForCredentials() bool } // Config is the configuration for the Gateway Claiming Server. @@ -43,8 +49,9 @@ type Config struct { } var ( - errInvalidUpstream = errors.DefineInvalidArgument("invalid_upstream", "upstream `{name}` is invalid") - errTTGCNotEnabled = errors.DefineFailedPrecondition("ttgc_not_enabled", "TTGC is not enabled") + errInvalidUpstream = errors.DefineInvalidArgument("invalid_upstream", "upstream `{name}` is invalid") + errTTGCNotEnabled = errors.DefineFailedPrecondition("ttgc_not_enabled", "TTGC is not enabled") + errLBSCUPSNotEnabled = errors.DefineFailedPrecondition("lbs_cups_not_enabled", "TTGC LBS CUPS is not enabled") ) // ParseGatewayEUIRanges parses the configured upstream map and returns map of ranges. @@ -134,6 +141,13 @@ func NewUpstream( } hosts["ttgc"] = ttgcRanges } + if _, lbscupsAdded := hosts["lbs-cups"]; ttgcConf.LBSCUPSEnabled && !lbscupsAdded { + lbscupsRanges := make([]dcstypes.EUI64Range, len(ttgcConf.LBSCUPSGatewayEUIs)) + for i, prefix := range ttgcConf.LBSCUPSGatewayEUIs { + lbscupsRanges[i] = dcstypes.RangeFromEUI64Prefix(prefix) + } + hosts["lbs-cups"] = lbscupsRanges + } // Setup upstream table. for name, ranges := range hosts { @@ -150,6 +164,14 @@ func NewUpstream( if err != nil { return nil, err } + case "lbs-cups": + if !ttgcConf.LBSCUPSEnabled { + return nil, errLBSCUPSNotEnabled.New() + } + claimer, err = lbscups.New(ctx, c, ttgcConf) + if err != nil { + return nil, err + } default: return nil, errInvalidUpstream.WithAttributes("name", name) } diff --git a/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go b/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go new file mode 100644 index 0000000000..63d42bcc72 --- /dev/null +++ b/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go @@ -0,0 +1,320 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package lbscups provides functions to claim gateways using LBS CUPS protocol. +package lbscups + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "net" + "time" + + northboundv1 "go.thethings.industries/pkg/api/gen/tti/gateway/controller/northbound/v1" + "go.thethings.network/lorawan-stack/v3/pkg/cluster" + "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" + dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/log" + "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" + "go.thethings.network/lorawan-stack/v3/pkg/ttgc" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const profileGroup = "tts" + +type component interface { + GetTLSConfig(context.Context) tlsconfig.Config + GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error) + GetPeerConn(ctx context.Context, role ttnpb.ClusterRole, ids cluster.EntityIdentifiers) (*grpc.ClientConn, error) + AllowInsecureForCredentials() bool +} + +// Upstream is the client for LBS CUPS gateway claiming. +type Upstream struct { + component + client *ttgc.Client + + gatewayAccess ttnpb.GatewayAccessClient +} + +// New returns a new upstream client for LBS CUPS gateway claiming. +func New(ctx context.Context, c component, config ttgc.Config) (*Upstream, error) { + client, err := ttgc.NewClient(ctx, c, config) + if err != nil { + return nil, err + } + upstream := &Upstream{ + component: c, + client: client, + } + return upstream, nil +} + +var errCreateAPIKey = errors.DefineAborted("create_api_key", "create API key") + +// Claim implements gateways.Claimer. +// Claim does the following: +// 1. Create CUPS and LNS API keys for the gateway +// 2. Claim the gateway on TTGC with the CUPS key as the gateway token +// 3. Return the LNS key in GatewayMetadata +func (u *Upstream) Claim( + ctx context.Context, eui types.EUI64, ownerToken, clusterAddress string, +) (*dcstypes.GatewayMetadata, error) { + logger := log.FromContext(ctx) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: eui.Bytes(), + } + + // Create CUPS and LNS API keys for the gateway. The CUPS key will be used as gateway token when claiming on TTGC and + // the LNS key will be returned in the metadata. The caller is responsible for updating the LNS key in the gateway. + cupsKey, lnsKey, err := u.createAPIKeys(ctx, ids) + if err != nil { + return nil, err + } + + // Claim the gateway on TTGC with the CUPS key as the gateway token. + gtwClient := northboundv1.NewGatewayServiceClient(u.client) + _, err = gtwClient.Claim(ctx, &northboundv1.GatewayServiceClaimRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + OwnerToken: ownerToken, + GatewayToken: []byte(cupsKey.Key), + }) + if err != nil { + logger.WithError(err).Warn("Failed to claim gateway on TTGC") + return nil, err + } + + // Get the Root CA from the Gateway Server. + host, _, err := net.SplitHostPort(clusterAddress) + if err != nil { + host = clusterAddress + } + clusterAddress = net.JoinHostPort(host, "8889") + rootCA, err := u.getRootCA(ctx, clusterAddress) + if err != nil { + return nil, err + } + + var ( + loraPFProfileID []byte + loraPFProfile = &northboundv1.LoraPacketForwarderProfile{ + ProfileName: clusterAddress, + Shared: false, + Protocol: northboundv1.LoraPacketForwarderProtocol_LORA_PACKET_FORWARDER_PROTOCOL_BASIC_STATION, + Address: clusterAddress, + RootCa: rootCA.Raw, + } + loraPFProfileClient = northboundv1.NewLoraPacketForwarderProfileServiceClient(u.client) + ) + loraPFGetRes, err := loraPFProfileClient.GetByName( + ctx, + &northboundv1.LoraPacketForwarderProfileServiceGetByNameRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + ProfileName: clusterAddress, + }, + ) + if err != nil { + if status.Code(err) != codes.NotFound { + logger.WithError(err).Warn("Failed to get LoRa Packet Forwarder profile") + return nil, err + } + res, err := loraPFProfileClient.Create(ctx, &northboundv1.LoraPacketForwarderProfileServiceCreateRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + LoraPacketForwarderProfile: loraPFProfile, + }) + if err != nil { + logger.WithError(err).Warn("Failed to create LoRa Packet Forwarder profile") + return nil, err + } + loraPFProfileID = res.ProfileId + } else { + if profile := loraPFGetRes.LoraPacketForwarderProfile; profile.Shared != loraPFProfile.Shared || + profile.Protocol != loraPFProfile.Protocol || + !bytes.Equal(profile.RootCa, loraPFProfile.RootCa) { + _, err := loraPFProfileClient.Update(ctx, &northboundv1.LoraPacketForwarderProfileServiceUpdateRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + ProfileId: loraPFGetRes.ProfileId, + LoraPacketForwarderProfile: loraPFProfile, + }) + if err != nil { + logger.WithError(err).Warn("Failed to update LoRa Packet Forwarder profile") + return nil, err + } + } + loraPFProfileID = loraPFGetRes.ProfileId + } + + // Update the gateway with the Lora Packet Forwarder profile. + _, err = gtwClient.Update(ctx, &northboundv1.GatewayServiceUpdateRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + LoraPacketForwarderProfileId: &northboundv1.ProfileIDValue{ + Value: loraPFProfileID, + }, + }) + if err != nil { + logger.WithError(err).Warn("Failed to update gateway with profiles") + return nil, err + } + + return &dcstypes.GatewayMetadata{ + LBSLNSKey: lnsKey, + }, nil +} + +// createAPIKeys creates the CUPS and LNS API keys for the gateway. +func (u *Upstream) createAPIKeys( + ctx context.Context, ids *ttnpb.GatewayIdentifiers, +) (cupsKey, lnsKey *ttnpb.APIKey, err error) { + logger := log.FromContext(ctx) + + gatewayAccess, err := u.getGatewayAccess(ctx) + if err != nil { + return nil, nil, err + } + + callOpt, err := rpcmetadata.WithForwardedAuth(ctx, u.AllowInsecureForCredentials()) + if err != nil { + return nil, nil, err + } + + cupsKey, err = gatewayAccess.CreateAPIKey(ctx, &ttnpb.CreateGatewayAPIKeyRequest{ + GatewayIds: ids, + Name: fmt.Sprintf("LBS CUPS Key (TTGC claim), generated %s", time.Now().UTC().Format(time.RFC3339)), + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_GATEWAY_INFO, + ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC, + ttnpb.Right_RIGHT_GATEWAY_READ_SECRETS, + }, + }, callOpt) + if err != nil { + logger.WithError(err).Warn("Failed to create CUPS API key") + return nil, nil, errCreateAPIKey.WithCause(err) + } + + lnsKey, err = gatewayAccess.CreateAPIKey(ctx, &ttnpb.CreateGatewayAPIKeyRequest{ + GatewayIds: ids, + Name: fmt.Sprintf("LBS LNS Key (TTGC claim), generated %s", time.Now().UTC().Format(time.RFC3339)), + Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_GATEWAY_LINK, + }, + }, callOpt) + if err != nil { + logger.WithError(err).Warn("Failed to create LNS API key") + return nil, nil, errCreateAPIKey.WithCause(err) + } + + return cupsKey, lnsKey, nil +} + +func (u *Upstream) getGatewayAccess(ctx context.Context) (ttnpb.GatewayAccessClient, error) { + if u.gatewayAccess != nil { + return u.gatewayAccess, nil + } + conn, err := u.GetPeerConn(ctx, ttnpb.ClusterRole_ACCESS, nil) + if err != nil { + return nil, err + } + return ttnpb.NewGatewayAccessClient(conn), nil +} + +// Unclaim implements gateways.Claimer. +// Unclaim revokes the API keys and unclaims the gateway on TTGC. +func (u *Upstream) Unclaim(ctx context.Context, eui types.EUI64) error { + ids := &ttnpb.GatewayIdentifiers{ + Eui: eui.Bytes(), + } + + if err := u.deleteAPIKeys(ctx, ids); err != nil { + return err + } + + // Unclaim the gateway on TTGC. + gtwClient := northboundv1.NewGatewayServiceClient(u.client) + _, err := gtwClient.Unclaim(ctx, &northboundv1.GatewayServiceUnclaimRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + }) + if err != nil { + if errors.IsNotFound(err) { + // The gateway does not exist or is already unclaimed. + return nil + } + return err + } + return nil +} + +var errDeleteAPIKey = errors.DefineAborted("delete_api_key", "delete API key") + +// deleteAPIKeys deletes the CUPS and LNS API keys for the gateway. +func (u *Upstream) deleteAPIKeys(ctx context.Context, ids *ttnpb.GatewayIdentifiers) error { + logger := log.FromContext(ctx) + + gatewayAccess, err := u.getGatewayAccess(ctx) + if err != nil { + return err + } + + callOpt, err := rpcmetadata.WithForwardedAuth(ctx, u.AllowInsecureForCredentials()) + if err != nil { + return err + } + + apiKeys, err := gatewayAccess.ListAPIKeys(ctx, &ttnpb.ListGatewayAPIKeysRequest{ + GatewayIds: ids, + }, callOpt) + if err != nil { + logger.WithError(err).Warn("Failed to list API keys") + return errDeleteAPIKey.WithCause(err) + } + + // Delete the LBS CUPS and LBS LNS keys. + for _, key := range apiKeys.ApiKeys { + if key.Name == "" { + continue + } + // Match keys created by this claimer. + if len(key.Name) > 8 && (key.Name[:8] == "LBS CUPS" || key.Name[:7] == "LBS LNS") { + _, err := gatewayAccess.DeleteAPIKey(ctx, &ttnpb.DeleteGatewayAPIKeyRequest{ + GatewayIds: ids, + KeyId: key.Id, + }, callOpt) + if err != nil { + logger.WithError(err).WithField("key_id", key.Id).Warn("Failed to delete API key") + // Continue deleting other keys. + } + } + } + + return nil +} + +// IsManagedGateway implements gateways.Claimer. +// This method always returns true. +func (*Upstream) IsManagedGateway(context.Context, types.EUI64) (bool, error) { + return true, nil +} diff --git a/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go b/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go new file mode 100644 index 0000000000..7a06d3a85b --- /dev/null +++ b/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go @@ -0,0 +1,59 @@ +// Copyright © 2025 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package lbscups provides functions to use The Things Gateway Controller LBS CUPS server. +package lbscups + +import ( + "context" + "crypto/tls" + "crypto/x509" + "net" + + "go.thethings.network/lorawan-stack/v3/pkg/errors" +) + +var ( + errDialGatewayServer = errors.DefineAborted("dial_gateway_server", "dial Gateway Server `{address}`") + errVerifyGatewayServerTLS = errors.DefineAborted( + "verify_gateway_server_tls", "verify TLS server certificate of Gateway Server `{address}`", + ) +) + +func (u *Upstream) getRootCA(ctx context.Context, address string) (*x509.Certificate, error) { + d := new(net.Dialer) + netConn, err := d.DialContext(ctx, "tcp", address) + if err != nil { + return nil, errDialGatewayServer.WithAttributes("address", address).WithCause(err) + } + defer netConn.Close() + + tlsConfig, err := u.GetTLSClientConfig(ctx) + if err != nil { + return nil, err + } + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + tlsConfig.ServerName = host + tlsConn := tls.Client(netConn, tlsConfig) + if err := tlsConn.HandshakeContext(ctx); err != nil { + return nil, errVerifyGatewayServerTLS.WithAttributes("address", address).WithCause(err) + } + + state := tlsConn.ConnectionState() + verifiedChain := state.VerifiedChains[0] + return verifiedChain[len(verifiedChain)-1], nil +} diff --git a/pkg/ttgc/config.go b/pkg/ttgc/config.go index 4e1dced72d..f32188129f 100644 --- a/pkg/ttgc/config.go +++ b/pkg/ttgc/config.go @@ -21,9 +21,11 @@ import ( // Config is the configuration for The Things Gateway Controller. type Config struct { - Enabled bool `name:"enabled" description:"Enable The Things Gateway Controller"` - GatewayEUIs []types.EUI64Prefix `name:"gateway-euis" description:"Gateway EUI prefixes that are managed by The Things Gateway Controller"` //nolint:lll - Address string `name:"address" description:"The address of The Things Gateway Controller"` - Domain string `name:"domain" description:"The domain of this cluster"` - TLS tlsconfig.ClientAuth `name:"tls" description:"TLS configuration"` + Enabled bool `name:"enabled" description:"Enable The Things Gateway Controller"` + GatewayEUIs []types.EUI64Prefix `name:"gateway-euis" description:"Gateway EUI prefixes that are managed by The Things Gateway Controller"` //nolint:lll + LBSCUPSEnabled bool `name:"lbs-cups-enabled" description:"Enable LBS CUPS protocol for gateway claiming"` + LBSCUPSGatewayEUIs []types.EUI64Prefix `name:"lbs-cups-gateway-euis" description:"Gateway EUI prefixes that are managed by LBS CUPS protocol"` //nolint:lll + Address string `name:"address" description:"The address of The Things Gateway Controller"` + Domain string `name:"domain" description:"The domain of this cluster"` + TLS tlsconfig.ClientAuth `name:"tls" description:"TLS configuration"` } From fd7301a6bef9af6d443701b9dc1e08346bdd48b2 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:22:35 +0200 Subject: [PATCH 04/10] util: Update the changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92660cd563..945e5c5b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,12 @@ For details about compatibility between different releases, see the **Commitment ### Added - Add tracing for LBS LNS and TTIGW protocol handlers. +- TTGC LBS Root CUPS claimer. ### Changed +- During the process of claiming a managed gateway, create the gateway in the registry before claiming it, not after. + ### Deprecated ### Removed From 72ede6c37b55d1a727bedc52d1577d22ce5fd1f3 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:49:54 +0200 Subject: [PATCH 05/10] dcs: Fix code quality errors --- pkg/deviceclaimingserver/gateways/lbscups/lbscups.go | 2 +- pkg/deviceclaimingserver/gateways/lbscups/root_ca.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go b/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go index 63d42bcc72..17d8840a4a 100644 --- a/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go +++ b/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go @@ -1,4 +1,4 @@ -// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2026 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go b/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go index 7a06d3a85b..6f9d1e4273 100644 --- a/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go +++ b/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go @@ -1,4 +1,4 @@ -// Copyright © 2025 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2026 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 5fc96b27bef2a87b6088674dd39577d5c1aadc61 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:57:21 +0200 Subject: [PATCH 06/10] dcs: Add LBS CUPS unit tests --- .../gateways/lbscups/lbscups_test.go | 443 ++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 pkg/deviceclaimingserver/gateways/lbscups/lbscups_test.go diff --git a/pkg/deviceclaimingserver/gateways/lbscups/lbscups_test.go b/pkg/deviceclaimingserver/gateways/lbscups/lbscups_test.go new file mode 100644 index 0000000000..33d52a657f --- /dev/null +++ b/pkg/deviceclaimingserver/gateways/lbscups/lbscups_test.go @@ -0,0 +1,443 @@ +// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lbscups + +import ( + "context" + "crypto/tls" + "fmt" + "strings" + "testing" + + "github.com/smarty/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/cluster" + "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" + "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/emptypb" +) + +var errTest = errors.DefineInternal("test", "test error") + +// testContext returns a context with authentication metadata for testing. +func testContext() context.Context { + return metadata.NewIncomingContext(test.Context(), metadata.Pairs("authorization", "Bearer test-token")) +} + +func TestCreateAPIKeys_Success(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + callCount := 0 + mockAccess := &mockGatewayAccessClient{ + createAPIKeyFunc: func(_ context.Context, req *ttnpb.CreateGatewayAPIKeyRequest, _ ...grpc.CallOption) (*ttnpb.APIKey, error) { + callCount++ + return &ttnpb.APIKey{ + Id: fmt.Sprintf("key-%d", callCount), + Key: fmt.Sprintf("secret-%d", callCount), + Name: req.Name, + Rights: req.Rights, + }, nil + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + cupsKey, lnsKey, err := upstream.createAPIKeys(testContext(), ids) + + a.So(err, should.BeNil) + a.So(cupsKey, should.NotBeNil) + a.So(cupsKey.Id, should.Equal, "key-1") + a.So(cupsKey.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_INFO) + a.So(cupsKey.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC) + a.So(cupsKey.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_READ_SECRETS) + a.So(lnsKey, should.NotBeNil) + a.So(lnsKey.Id, should.Equal, "key-2") + a.So(lnsKey.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_LINK) + a.So(len(lnsKey.Rights), should.Equal, 1) +} + +func TestCreateAPIKeys_CUPSKeyCreationFails(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + mockAccess := &mockGatewayAccessClient{ + createAPIKeyFunc: func(_ context.Context, _ *ttnpb.CreateGatewayAPIKeyRequest, _ ...grpc.CallOption) (*ttnpb.APIKey, error) { + return nil, errTest.New() + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + _, _, err := upstream.createAPIKeys(testContext(), ids) + + a.So(errors.IsAborted(err), should.BeTrue) +} + +func TestCreateAPIKeys_LNSKeyCreationFails(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + callCount := 0 + mockAccess := &mockGatewayAccessClient{ + createAPIKeyFunc: func(_ context.Context, req *ttnpb.CreateGatewayAPIKeyRequest, _ ...grpc.CallOption) (*ttnpb.APIKey, error) { + callCount++ + if callCount == 1 { + return &ttnpb.APIKey{ + Id: "cups-key", + Key: "cups-secret", + Name: req.Name, + Rights: req.Rights, + }, nil + } + return nil, errTest.New() + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + _, _, err := upstream.createAPIKeys(testContext(), ids) + + a.So(errors.IsAborted(err), should.BeTrue) +} + +func TestCreateAPIKeys_VerifyRequestedRights(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + var requests []*ttnpb.CreateGatewayAPIKeyRequest + mockAccess := &mockGatewayAccessClient{ + createAPIKeyFunc: func(_ context.Context, req *ttnpb.CreateGatewayAPIKeyRequest, _ ...grpc.CallOption) (*ttnpb.APIKey, error) { + requests = append(requests, req) + return &ttnpb.APIKey{ + Id: "key", + Key: "secret", + Name: req.Name, + Rights: req.Rights, + }, nil + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + _, _, err := upstream.createAPIKeys(testContext(), ids) + + a.So(err, should.BeNil) + a.So(len(requests), should.Equal, 2) + + // First request should be CUPS key + cupsReq := requests[0] + a.So(strings.HasPrefix(cupsReq.Name, "LBS CUPS Key"), should.BeTrue) + a.So(cupsReq.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_INFO) + a.So(cupsReq.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC) + a.So(cupsReq.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_READ_SECRETS) + a.So(len(cupsReq.Rights), should.Equal, 3) + + // Second request should be LNS key + lnsReq := requests[1] + a.So(strings.HasPrefix(lnsReq.Name, "LBS LNS Key"), should.BeTrue) + a.So(lnsReq.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_LINK) + a.So(len(lnsReq.Rights), should.Equal, 1) +} + +func TestDeleteAPIKeys_Success(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + existingKeys := []*ttnpb.APIKey{ + {Id: "cups-key-1", Name: "LBS CUPS Key (TTGC claim), generated 2024-01-01"}, + {Id: "lns-key-1", Name: "LBS LNS Key (TTGC claim), generated 2024-01-01"}, + {Id: "other-key", Name: "Some other key"}, + } + + var deletedKeyIDs []string + mockAccess := &mockGatewayAccessClient{ + listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { + return &ttnpb.APIKeys{ApiKeys: existingKeys}, nil + }, + deleteAPIKeyFunc: func(_ context.Context, req *ttnpb.DeleteGatewayAPIKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + deletedKeyIDs = append(deletedKeyIDs, req.KeyId) + return &emptypb.Empty{}, nil + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + err := upstream.deleteAPIKeys(testContext(), ids) + + a.So(err, should.BeNil) + a.So(len(deletedKeyIDs), should.Equal, 2) + a.So(deletedKeyIDs, should.Contain, "cups-key-1") + a.So(deletedKeyIDs, should.Contain, "lns-key-1") +} + +func TestDeleteAPIKeys_ListAPIKeysFails(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + mockAccess := &mockGatewayAccessClient{ + listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { + return nil, errTest.New() + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + err := upstream.deleteAPIKeys(testContext(), ids) + + a.So(errors.IsAborted(err), should.BeTrue) +} + +func TestDeleteAPIKeys_DeleteAPIKeyFailsContinues(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + existingKeys := []*ttnpb.APIKey{ + {Id: "cups-key-1", Name: "LBS CUPS Key"}, + {Id: "lns-key-1", Name: "LBS LNS Key"}, + } + + var deletedKeyIDs []string + callCount := 0 + mockAccess := &mockGatewayAccessClient{ + listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { + return &ttnpb.APIKeys{ApiKeys: existingKeys}, nil + }, + deleteAPIKeyFunc: func(_ context.Context, req *ttnpb.DeleteGatewayAPIKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + deletedKeyIDs = append(deletedKeyIDs, req.KeyId) + callCount++ + if callCount == 1 { + return nil, errTest.New() + } + return &emptypb.Empty{}, nil + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + err := upstream.deleteAPIKeys(testContext(), ids) + + a.So(err, should.BeNil) + // Both keys should be attempted to delete (even if first fails) + a.So(len(deletedKeyIDs), should.Equal, 2) +} + +func TestDeleteAPIKeys_SkipsEmptyNames(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + existingKeys := []*ttnpb.APIKey{ + {Id: "empty-name-key", Name: ""}, + {Id: "cups-key-1", Name: "LBS CUPS Key"}, + } + + var deletedKeyIDs []string + mockAccess := &mockGatewayAccessClient{ + listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { + return &ttnpb.APIKeys{ApiKeys: existingKeys}, nil + }, + deleteAPIKeyFunc: func(_ context.Context, req *ttnpb.DeleteGatewayAPIKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + deletedKeyIDs = append(deletedKeyIDs, req.KeyId) + return &emptypb.Empty{}, nil + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + err := upstream.deleteAPIKeys(testContext(), ids) + + a.So(err, should.BeNil) + a.So(len(deletedKeyIDs), should.Equal, 1) + a.So(deletedKeyIDs, should.Contain, "cups-key-1") + a.So(deletedKeyIDs, should.NotContain, "empty-name-key") +} + +func TestDeleteAPIKeys_SkipsNonLBSKeys(t *testing.T) { + t.Parallel() + a := assertions.New(t) + + ids := &ttnpb.GatewayIdentifiers{ + Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), + } + + existingKeys := []*ttnpb.APIKey{ + {Id: "other-key-1", Name: "Console generated key"}, + {Id: "other-key-2", Name: "CLI generated key"}, + {Id: "cups-key", Name: "LBS CUPS Key"}, + } + + var deletedKeyIDs []string + mockAccess := &mockGatewayAccessClient{ + listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { + return &ttnpb.APIKeys{ApiKeys: existingKeys}, nil + }, + deleteAPIKeyFunc: func(_ context.Context, req *ttnpb.DeleteGatewayAPIKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + deletedKeyIDs = append(deletedKeyIDs, req.KeyId) + return &emptypb.Empty{}, nil + }, + } + + upstream := &Upstream{ + component: &mockComponent{allowInsecureFunc: func() bool { return true }}, + gatewayAccess: mockAccess, + } + + err := upstream.deleteAPIKeys(testContext(), ids) + + a.So(err, should.BeNil) + a.So(len(deletedKeyIDs), should.Equal, 1) + a.So(deletedKeyIDs, should.Contain, "cups-key") +} + +// mockGatewayAccessClient implements ttnpb.GatewayAccessClient for testing. +type mockGatewayAccessClient struct { + ttnpb.GatewayAccessClient + + createAPIKeyFunc func( + ctx context.Context, + req *ttnpb.CreateGatewayAPIKeyRequest, + opts ...grpc.CallOption, + ) (*ttnpb.APIKey, error) + listAPIKeysFunc func( + ctx context.Context, + req *ttnpb.ListGatewayAPIKeysRequest, + opts ...grpc.CallOption, + ) (*ttnpb.APIKeys, error) + deleteAPIKeyFunc func( + ctx context.Context, + req *ttnpb.DeleteGatewayAPIKeyRequest, + opts ...grpc.CallOption, + ) (*emptypb.Empty, error) +} + +func (m *mockGatewayAccessClient) CreateAPIKey( + ctx context.Context, + req *ttnpb.CreateGatewayAPIKeyRequest, + opts ...grpc.CallOption, +) (*ttnpb.APIKey, error) { + if m.createAPIKeyFunc != nil { + return m.createAPIKeyFunc(ctx, req, opts...) + } + return nil, nil +} + +func (m *mockGatewayAccessClient) ListAPIKeys( + ctx context.Context, + req *ttnpb.ListGatewayAPIKeysRequest, + opts ...grpc.CallOption, +) (*ttnpb.APIKeys, error) { + if m.listAPIKeysFunc != nil { + return m.listAPIKeysFunc(ctx, req, opts...) + } + return nil, nil +} + +func (m *mockGatewayAccessClient) DeleteAPIKey( + ctx context.Context, + req *ttnpb.DeleteGatewayAPIKeyRequest, + opts ...grpc.CallOption, +) (*emptypb.Empty, error) { + if m.deleteAPIKeyFunc != nil { + return m.deleteAPIKeyFunc(ctx, req, opts...) + } + return nil, nil +} + +// mockComponent implements the component interface for testing. +type mockComponent struct { + allowInsecureFunc func() bool +} + +func (m *mockComponent) GetTLSConfig(context.Context) tlsconfig.Config { + return tlsconfig.Config{} +} + +func (m *mockComponent) GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error) { + return nil, nil +} + +func (m *mockComponent) GetPeerConn( + context.Context, ttnpb.ClusterRole, cluster.EntityIdentifiers, +) (*grpc.ClientConn, error) { + return nil, nil +} + +func (m *mockComponent) AllowInsecureForCredentials() bool { + if m.allowInsecureFunc != nil { + return m.allowInsecureFunc() + } + return true +} From 9aaa3815271e89ed71ce4f5794c8981db35234f8 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:28:20 +0200 Subject: [PATCH 07/10] dcs: Drop the separate lbs claimer, update the ttgc claimer --- CHANGELOG.md | 2 +- pkg/deviceclaimingserver/gateways/gateways.go | 21 +- .../gateways/lbscups/lbscups_test.go | 443 ------------------ .../gateways/lbscups/root_ca.go | 59 --- .../gateways/{lbscups => ttgc}/lbscups.go | 85 +--- .../gateways/ttgc/ttgc.go | 181 ++----- .../gateways/ttgc/ttiv1.go | 189 ++++++++ pkg/ttgc/config.go | 12 +- 8 files changed, 239 insertions(+), 753 deletions(-) delete mode 100644 pkg/deviceclaimingserver/gateways/lbscups/lbscups_test.go delete mode 100644 pkg/deviceclaimingserver/gateways/lbscups/root_ca.go rename pkg/deviceclaimingserver/gateways/{lbscups => ttgc}/lbscups.go (76%) create mode 100644 pkg/deviceclaimingserver/gateways/ttgc/ttiv1.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 945e5c5b41..564b57033b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ For details about compatibility between different releases, see the **Commitment ### Added - Add tracing for LBS LNS and TTIGW protocol handlers. -- TTGC LBS Root CUPS claimer. +- TTGC LBS Root CUPS claiming support. ### Changed diff --git a/pkg/deviceclaimingserver/gateways/gateways.go b/pkg/deviceclaimingserver/gateways/gateways.go index e56036b8cd..919995f679 100644 --- a/pkg/deviceclaimingserver/gateways/gateways.go +++ b/pkg/deviceclaimingserver/gateways/gateways.go @@ -23,7 +23,6 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/cluster" "go.thethings.network/lorawan-stack/v3/pkg/config" "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" - "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways/lbscups" "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways/ttgc" dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types" "go.thethings.network/lorawan-stack/v3/pkg/errors" @@ -49,9 +48,8 @@ type Config struct { } var ( - errInvalidUpstream = errors.DefineInvalidArgument("invalid_upstream", "upstream `{name}` is invalid") - errTTGCNotEnabled = errors.DefineFailedPrecondition("ttgc_not_enabled", "TTGC is not enabled") - errLBSCUPSNotEnabled = errors.DefineFailedPrecondition("lbs_cups_not_enabled", "TTGC LBS CUPS is not enabled") + errInvalidUpstream = errors.DefineInvalidArgument("invalid_upstream", "upstream `{name}` is invalid") + errTTGCNotEnabled = errors.DefineFailedPrecondition("ttgc_not_enabled", "TTGC is not enabled") ) // ParseGatewayEUIRanges parses the configured upstream map and returns map of ranges. @@ -141,13 +139,6 @@ func NewUpstream( } hosts["ttgc"] = ttgcRanges } - if _, lbscupsAdded := hosts["lbs-cups"]; ttgcConf.LBSCUPSEnabled && !lbscupsAdded { - lbscupsRanges := make([]dcstypes.EUI64Range, len(ttgcConf.LBSCUPSGatewayEUIs)) - for i, prefix := range ttgcConf.LBSCUPSGatewayEUIs { - lbscupsRanges[i] = dcstypes.RangeFromEUI64Prefix(prefix) - } - hosts["lbs-cups"] = lbscupsRanges - } // Setup upstream table. for name, ranges := range hosts { @@ -164,14 +155,6 @@ func NewUpstream( if err != nil { return nil, err } - case "lbs-cups": - if !ttgcConf.LBSCUPSEnabled { - return nil, errLBSCUPSNotEnabled.New() - } - claimer, err = lbscups.New(ctx, c, ttgcConf) - if err != nil { - return nil, err - } default: return nil, errInvalidUpstream.WithAttributes("name", name) } diff --git a/pkg/deviceclaimingserver/gateways/lbscups/lbscups_test.go b/pkg/deviceclaimingserver/gateways/lbscups/lbscups_test.go deleted file mode 100644 index 33d52a657f..0000000000 --- a/pkg/deviceclaimingserver/gateways/lbscups/lbscups_test.go +++ /dev/null @@ -1,443 +0,0 @@ -// Copyright © 2024 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package lbscups - -import ( - "context" - "crypto/tls" - "fmt" - "strings" - "testing" - - "github.com/smarty/assertions" - "go.thethings.network/lorawan-stack/v3/pkg/cluster" - "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" - "go.thethings.network/lorawan-stack/v3/pkg/errors" - "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" - "go.thethings.network/lorawan-stack/v3/pkg/types" - "go.thethings.network/lorawan-stack/v3/pkg/util/test" - "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/protobuf/types/known/emptypb" -) - -var errTest = errors.DefineInternal("test", "test error") - -// testContext returns a context with authentication metadata for testing. -func testContext() context.Context { - return metadata.NewIncomingContext(test.Context(), metadata.Pairs("authorization", "Bearer test-token")) -} - -func TestCreateAPIKeys_Success(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - callCount := 0 - mockAccess := &mockGatewayAccessClient{ - createAPIKeyFunc: func(_ context.Context, req *ttnpb.CreateGatewayAPIKeyRequest, _ ...grpc.CallOption) (*ttnpb.APIKey, error) { - callCount++ - return &ttnpb.APIKey{ - Id: fmt.Sprintf("key-%d", callCount), - Key: fmt.Sprintf("secret-%d", callCount), - Name: req.Name, - Rights: req.Rights, - }, nil - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - cupsKey, lnsKey, err := upstream.createAPIKeys(testContext(), ids) - - a.So(err, should.BeNil) - a.So(cupsKey, should.NotBeNil) - a.So(cupsKey.Id, should.Equal, "key-1") - a.So(cupsKey.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_INFO) - a.So(cupsKey.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC) - a.So(cupsKey.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_READ_SECRETS) - a.So(lnsKey, should.NotBeNil) - a.So(lnsKey.Id, should.Equal, "key-2") - a.So(lnsKey.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_LINK) - a.So(len(lnsKey.Rights), should.Equal, 1) -} - -func TestCreateAPIKeys_CUPSKeyCreationFails(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - mockAccess := &mockGatewayAccessClient{ - createAPIKeyFunc: func(_ context.Context, _ *ttnpb.CreateGatewayAPIKeyRequest, _ ...grpc.CallOption) (*ttnpb.APIKey, error) { - return nil, errTest.New() - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - _, _, err := upstream.createAPIKeys(testContext(), ids) - - a.So(errors.IsAborted(err), should.BeTrue) -} - -func TestCreateAPIKeys_LNSKeyCreationFails(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - callCount := 0 - mockAccess := &mockGatewayAccessClient{ - createAPIKeyFunc: func(_ context.Context, req *ttnpb.CreateGatewayAPIKeyRequest, _ ...grpc.CallOption) (*ttnpb.APIKey, error) { - callCount++ - if callCount == 1 { - return &ttnpb.APIKey{ - Id: "cups-key", - Key: "cups-secret", - Name: req.Name, - Rights: req.Rights, - }, nil - } - return nil, errTest.New() - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - _, _, err := upstream.createAPIKeys(testContext(), ids) - - a.So(errors.IsAborted(err), should.BeTrue) -} - -func TestCreateAPIKeys_VerifyRequestedRights(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - var requests []*ttnpb.CreateGatewayAPIKeyRequest - mockAccess := &mockGatewayAccessClient{ - createAPIKeyFunc: func(_ context.Context, req *ttnpb.CreateGatewayAPIKeyRequest, _ ...grpc.CallOption) (*ttnpb.APIKey, error) { - requests = append(requests, req) - return &ttnpb.APIKey{ - Id: "key", - Key: "secret", - Name: req.Name, - Rights: req.Rights, - }, nil - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - _, _, err := upstream.createAPIKeys(testContext(), ids) - - a.So(err, should.BeNil) - a.So(len(requests), should.Equal, 2) - - // First request should be CUPS key - cupsReq := requests[0] - a.So(strings.HasPrefix(cupsReq.Name, "LBS CUPS Key"), should.BeTrue) - a.So(cupsReq.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_INFO) - a.So(cupsReq.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC) - a.So(cupsReq.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_READ_SECRETS) - a.So(len(cupsReq.Rights), should.Equal, 3) - - // Second request should be LNS key - lnsReq := requests[1] - a.So(strings.HasPrefix(lnsReq.Name, "LBS LNS Key"), should.BeTrue) - a.So(lnsReq.Rights, should.Contain, ttnpb.Right_RIGHT_GATEWAY_LINK) - a.So(len(lnsReq.Rights), should.Equal, 1) -} - -func TestDeleteAPIKeys_Success(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - existingKeys := []*ttnpb.APIKey{ - {Id: "cups-key-1", Name: "LBS CUPS Key (TTGC claim), generated 2024-01-01"}, - {Id: "lns-key-1", Name: "LBS LNS Key (TTGC claim), generated 2024-01-01"}, - {Id: "other-key", Name: "Some other key"}, - } - - var deletedKeyIDs []string - mockAccess := &mockGatewayAccessClient{ - listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { - return &ttnpb.APIKeys{ApiKeys: existingKeys}, nil - }, - deleteAPIKeyFunc: func(_ context.Context, req *ttnpb.DeleteGatewayAPIKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { - deletedKeyIDs = append(deletedKeyIDs, req.KeyId) - return &emptypb.Empty{}, nil - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - err := upstream.deleteAPIKeys(testContext(), ids) - - a.So(err, should.BeNil) - a.So(len(deletedKeyIDs), should.Equal, 2) - a.So(deletedKeyIDs, should.Contain, "cups-key-1") - a.So(deletedKeyIDs, should.Contain, "lns-key-1") -} - -func TestDeleteAPIKeys_ListAPIKeysFails(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - mockAccess := &mockGatewayAccessClient{ - listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { - return nil, errTest.New() - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - err := upstream.deleteAPIKeys(testContext(), ids) - - a.So(errors.IsAborted(err), should.BeTrue) -} - -func TestDeleteAPIKeys_DeleteAPIKeyFailsContinues(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - existingKeys := []*ttnpb.APIKey{ - {Id: "cups-key-1", Name: "LBS CUPS Key"}, - {Id: "lns-key-1", Name: "LBS LNS Key"}, - } - - var deletedKeyIDs []string - callCount := 0 - mockAccess := &mockGatewayAccessClient{ - listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { - return &ttnpb.APIKeys{ApiKeys: existingKeys}, nil - }, - deleteAPIKeyFunc: func(_ context.Context, req *ttnpb.DeleteGatewayAPIKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { - deletedKeyIDs = append(deletedKeyIDs, req.KeyId) - callCount++ - if callCount == 1 { - return nil, errTest.New() - } - return &emptypb.Empty{}, nil - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - err := upstream.deleteAPIKeys(testContext(), ids) - - a.So(err, should.BeNil) - // Both keys should be attempted to delete (even if first fails) - a.So(len(deletedKeyIDs), should.Equal, 2) -} - -func TestDeleteAPIKeys_SkipsEmptyNames(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - existingKeys := []*ttnpb.APIKey{ - {Id: "empty-name-key", Name: ""}, - {Id: "cups-key-1", Name: "LBS CUPS Key"}, - } - - var deletedKeyIDs []string - mockAccess := &mockGatewayAccessClient{ - listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { - return &ttnpb.APIKeys{ApiKeys: existingKeys}, nil - }, - deleteAPIKeyFunc: func(_ context.Context, req *ttnpb.DeleteGatewayAPIKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { - deletedKeyIDs = append(deletedKeyIDs, req.KeyId) - return &emptypb.Empty{}, nil - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - err := upstream.deleteAPIKeys(testContext(), ids) - - a.So(err, should.BeNil) - a.So(len(deletedKeyIDs), should.Equal, 1) - a.So(deletedKeyIDs, should.Contain, "cups-key-1") - a.So(deletedKeyIDs, should.NotContain, "empty-name-key") -} - -func TestDeleteAPIKeys_SkipsNonLBSKeys(t *testing.T) { - t.Parallel() - a := assertions.New(t) - - ids := &ttnpb.GatewayIdentifiers{ - Eui: types.EUI64{0x58, 0xa0, 0xcb, 0xff, 0xfe, 0x80, 0x00, 0x01}.Bytes(), - } - - existingKeys := []*ttnpb.APIKey{ - {Id: "other-key-1", Name: "Console generated key"}, - {Id: "other-key-2", Name: "CLI generated key"}, - {Id: "cups-key", Name: "LBS CUPS Key"}, - } - - var deletedKeyIDs []string - mockAccess := &mockGatewayAccessClient{ - listAPIKeysFunc: func(_ context.Context, _ *ttnpb.ListGatewayAPIKeysRequest, _ ...grpc.CallOption) (*ttnpb.APIKeys, error) { - return &ttnpb.APIKeys{ApiKeys: existingKeys}, nil - }, - deleteAPIKeyFunc: func(_ context.Context, req *ttnpb.DeleteGatewayAPIKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { - deletedKeyIDs = append(deletedKeyIDs, req.KeyId) - return &emptypb.Empty{}, nil - }, - } - - upstream := &Upstream{ - component: &mockComponent{allowInsecureFunc: func() bool { return true }}, - gatewayAccess: mockAccess, - } - - err := upstream.deleteAPIKeys(testContext(), ids) - - a.So(err, should.BeNil) - a.So(len(deletedKeyIDs), should.Equal, 1) - a.So(deletedKeyIDs, should.Contain, "cups-key") -} - -// mockGatewayAccessClient implements ttnpb.GatewayAccessClient for testing. -type mockGatewayAccessClient struct { - ttnpb.GatewayAccessClient - - createAPIKeyFunc func( - ctx context.Context, - req *ttnpb.CreateGatewayAPIKeyRequest, - opts ...grpc.CallOption, - ) (*ttnpb.APIKey, error) - listAPIKeysFunc func( - ctx context.Context, - req *ttnpb.ListGatewayAPIKeysRequest, - opts ...grpc.CallOption, - ) (*ttnpb.APIKeys, error) - deleteAPIKeyFunc func( - ctx context.Context, - req *ttnpb.DeleteGatewayAPIKeyRequest, - opts ...grpc.CallOption, - ) (*emptypb.Empty, error) -} - -func (m *mockGatewayAccessClient) CreateAPIKey( - ctx context.Context, - req *ttnpb.CreateGatewayAPIKeyRequest, - opts ...grpc.CallOption, -) (*ttnpb.APIKey, error) { - if m.createAPIKeyFunc != nil { - return m.createAPIKeyFunc(ctx, req, opts...) - } - return nil, nil -} - -func (m *mockGatewayAccessClient) ListAPIKeys( - ctx context.Context, - req *ttnpb.ListGatewayAPIKeysRequest, - opts ...grpc.CallOption, -) (*ttnpb.APIKeys, error) { - if m.listAPIKeysFunc != nil { - return m.listAPIKeysFunc(ctx, req, opts...) - } - return nil, nil -} - -func (m *mockGatewayAccessClient) DeleteAPIKey( - ctx context.Context, - req *ttnpb.DeleteGatewayAPIKeyRequest, - opts ...grpc.CallOption, -) (*emptypb.Empty, error) { - if m.deleteAPIKeyFunc != nil { - return m.deleteAPIKeyFunc(ctx, req, opts...) - } - return nil, nil -} - -// mockComponent implements the component interface for testing. -type mockComponent struct { - allowInsecureFunc func() bool -} - -func (m *mockComponent) GetTLSConfig(context.Context) tlsconfig.Config { - return tlsconfig.Config{} -} - -func (m *mockComponent) GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error) { - return nil, nil -} - -func (m *mockComponent) GetPeerConn( - context.Context, ttnpb.ClusterRole, cluster.EntityIdentifiers, -) (*grpc.ClientConn, error) { - return nil, nil -} - -func (m *mockComponent) AllowInsecureForCredentials() bool { - if m.allowInsecureFunc != nil { - return m.allowInsecureFunc() - } - return true -} diff --git a/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go b/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go deleted file mode 100644 index 6f9d1e4273..0000000000 --- a/pkg/deviceclaimingserver/gateways/lbscups/root_ca.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright © 2026 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package lbscups provides functions to use The Things Gateway Controller LBS CUPS server. -package lbscups - -import ( - "context" - "crypto/tls" - "crypto/x509" - "net" - - "go.thethings.network/lorawan-stack/v3/pkg/errors" -) - -var ( - errDialGatewayServer = errors.DefineAborted("dial_gateway_server", "dial Gateway Server `{address}`") - errVerifyGatewayServerTLS = errors.DefineAborted( - "verify_gateway_server_tls", "verify TLS server certificate of Gateway Server `{address}`", - ) -) - -func (u *Upstream) getRootCA(ctx context.Context, address string) (*x509.Certificate, error) { - d := new(net.Dialer) - netConn, err := d.DialContext(ctx, "tcp", address) - if err != nil { - return nil, errDialGatewayServer.WithAttributes("address", address).WithCause(err) - } - defer netConn.Close() - - tlsConfig, err := u.GetTLSClientConfig(ctx) - if err != nil { - return nil, err - } - host, _, err := net.SplitHostPort(address) - if err != nil { - return nil, err - } - tlsConfig.ServerName = host - tlsConn := tls.Client(netConn, tlsConfig) - if err := tlsConn.HandshakeContext(ctx); err != nil { - return nil, errVerifyGatewayServerTLS.WithAttributes("address", address).WithCause(err) - } - - state := tlsConn.ConnectionState() - verifiedChain := state.VerifiedChains[0] - return verifiedChain[len(verifiedChain)-1], nil -} diff --git a/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go b/pkg/deviceclaimingserver/gateways/ttgc/lbscups.go similarity index 76% rename from pkg/deviceclaimingserver/gateways/lbscups/lbscups.go rename to pkg/deviceclaimingserver/gateways/ttgc/lbscups.go index 17d8840a4a..4d79613f80 100644 --- a/pkg/deviceclaimingserver/gateways/lbscups/lbscups.go +++ b/pkg/deviceclaimingserver/gateways/ttgc/lbscups.go @@ -12,70 +12,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package lbscups provides functions to claim gateways using LBS CUPS protocol. -package lbscups +package ttgc import ( "bytes" "context" - "crypto/tls" "fmt" "net" "time" northboundv1 "go.thethings.industries/pkg/api/gen/tti/gateway/controller/northbound/v1" - "go.thethings.network/lorawan-stack/v3/pkg/cluster" - "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types" "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/log" "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" - "go.thethings.network/lorawan-stack/v3/pkg/ttgc" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" - "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -const profileGroup = "tts" - -type component interface { - GetTLSConfig(context.Context) tlsconfig.Config - GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error) - GetPeerConn(ctx context.Context, role ttnpb.ClusterRole, ids cluster.EntityIdentifiers) (*grpc.ClientConn, error) - AllowInsecureForCredentials() bool -} - -// Upstream is the client for LBS CUPS gateway claiming. -type Upstream struct { - component - client *ttgc.Client - - gatewayAccess ttnpb.GatewayAccessClient -} - -// New returns a new upstream client for LBS CUPS gateway claiming. -func New(ctx context.Context, c component, config ttgc.Config) (*Upstream, error) { - client, err := ttgc.NewClient(ctx, c, config) - if err != nil { - return nil, err - } - upstream := &Upstream{ - component: c, - client: client, - } - return upstream, nil -} - -var errCreateAPIKey = errors.DefineAborted("create_api_key", "create API key") +var ( + errCreateAPIKey = errors.DefineFailedPrecondition("create_api_key", "failed to create API key for gateway") + errDeleteAPIKey = errors.DefineAborted("delete_api_key", "delete API key") +) -// Claim implements gateways.Claimer. -// Claim does the following: -// 1. Create CUPS and LNS API keys for the gateway -// 2. Claim the gateway on TTGC with the CUPS key as the gateway token -// 3. Return the LNS key in GatewayMetadata -func (u *Upstream) Claim( +func (u *Upstream) claimLBSCUPSGateway( ctx context.Context, eui types.EUI64, ownerToken, clusterAddress string, ) (*dcstypes.GatewayMetadata, error) { logger := log.FromContext(ctx) @@ -241,35 +203,6 @@ func (u *Upstream) getGatewayAccess(ctx context.Context) (ttnpb.GatewayAccessCli return ttnpb.NewGatewayAccessClient(conn), nil } -// Unclaim implements gateways.Claimer. -// Unclaim revokes the API keys and unclaims the gateway on TTGC. -func (u *Upstream) Unclaim(ctx context.Context, eui types.EUI64) error { - ids := &ttnpb.GatewayIdentifiers{ - Eui: eui.Bytes(), - } - - if err := u.deleteAPIKeys(ctx, ids); err != nil { - return err - } - - // Unclaim the gateway on TTGC. - gtwClient := northboundv1.NewGatewayServiceClient(u.client) - _, err := gtwClient.Unclaim(ctx, &northboundv1.GatewayServiceUnclaimRequest{ - GatewayId: eui.MarshalNumber(), - Domain: u.client.Domain(ctx), - }) - if err != nil { - if errors.IsNotFound(err) { - // The gateway does not exist or is already unclaimed. - return nil - } - return err - } - return nil -} - -var errDeleteAPIKey = errors.DefineAborted("delete_api_key", "delete API key") - // deleteAPIKeys deletes the CUPS and LNS API keys for the gateway. func (u *Upstream) deleteAPIKeys(ctx context.Context, ids *ttnpb.GatewayIdentifiers) error { logger := log.FromContext(ctx) @@ -312,9 +245,3 @@ func (u *Upstream) deleteAPIKeys(ctx context.Context, ids *ttnpb.GatewayIdentifi return nil } - -// IsManagedGateway implements gateways.Claimer. -// This method always returns true. -func (*Upstream) IsManagedGateway(context.Context, types.EUI64) (bool, error) { - return true, nil -} diff --git a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go index 8b35fbd695..e5a76d849f 100644 --- a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go +++ b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go @@ -16,12 +16,11 @@ package ttgc import ( - "bytes" "context" "crypto/tls" - "net" northboundv1 "go.thethings.industries/pkg/api/gen/tti/gateway/controller/northbound/v1" + "go.thethings.network/lorawan-stack/v3/pkg/cluster" "go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig" dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types" "go.thethings.network/lorawan-stack/v3/pkg/errors" @@ -29,25 +28,33 @@ import ( "go.thethings.network/lorawan-stack/v3/pkg/ttgc" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/grpc" +) + +var ( + errNoSupportedProtocol = errors.DefineFailedPrecondition("no_supported_protocol", "no supported gateway protocol found for claiming") + errNoSupportedAuthMethod = errors.DefineFailedPrecondition("no_supported_auth_method", "no supported authentication method found for gateway") ) const profileGroup = "tts" type component interface { + GetTLSConfig(context.Context) tlsconfig.Config GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error) + GetPeerConn(ctx context.Context, role ttnpb.ClusterRole, ids cluster.EntityIdentifiers) (*grpc.ClientConn, error) + AllowInsecureForCredentials() bool } // Upstream is the client for The Things Gateway Controller. type Upstream struct { component client *ttgc.Client + + gatewayAccess ttnpb.GatewayAccessClient } // New returns a new upstream client for The Things Gateway Controller. -func New(ctx context.Context, c ttgc.Component, config ttgc.Config) (*Upstream, error) { +func New(ctx context.Context, c component, config ttgc.Config) (*Upstream, error) { client, err := ttgc.NewClient(ctx, c, config) if err != nil { return nil, err @@ -59,166 +66,50 @@ func New(ctx context.Context, c ttgc.Component, config ttgc.Config) (*Upstream, } // Claim implements gateways.GatewayClaimer. -// Claim does four things: -// 1. Claim the gateway -// 2. Upsert a LoRa Packet Forwarder profile with the root CA presented by the given Gateway Server -// 3. Upsert a Geolocation profile -// 4. Update the gateway with the profiles func (u *Upstream) Claim( ctx context.Context, eui types.EUI64, ownerToken, clusterAddress string, ) (*dcstypes.GatewayMetadata, error) { - logger := log.FromContext(ctx) - - // Claim the gateway. + // Get the gateway description to verify what protocol it supports. gtwClient := northboundv1.NewGatewayServiceClient(u.client) - _, err := gtwClient.Claim(ctx, &northboundv1.GatewayServiceClaimRequest{ - GatewayId: eui.MarshalNumber(), - Domain: u.client.Domain(ctx), - OwnerToken: ownerToken, + desc, err := gtwClient.Describe(ctx, &northboundv1.GatewayServiceDescribeRequest{ + GatewayId: eui.MarshalNumber(), }) if err != nil { return nil, err } - // Get the root CA from the Gateway Server and upsert the LoRa Packet Forwarder profile. - host, _, err := net.SplitHostPort(clusterAddress) - if err != nil { - host = clusterAddress - } - clusterAddress = net.JoinHostPort(host, "8889") - rootCA, err := u.getRootCA(ctx, clusterAddress) - if err != nil { - return nil, err - } - var ( - loraPFProfileID []byte - loraPFProfile = &northboundv1.LoraPacketForwarderProfile{ - ProfileName: clusterAddress, - Shared: true, - Protocol: northboundv1.LoraPacketForwarderProtocol_LORA_PACKET_FORWARDER_PROTOCOL_TTI_V1, - Address: clusterAddress, - RootCa: rootCA.Raw, - } - loraPFProfileClient = northboundv1.NewLoraPacketForwarderProfileServiceClient(u.client) - ) - loraPFGetRes, err := loraPFProfileClient.GetByName( - ctx, - &northboundv1.LoraPacketForwarderProfileServiceGetByNameRequest{ - Domain: u.client.Domain(ctx), - Group: profileGroup, - ProfileName: clusterAddress, - }, - ) - if err != nil { - if status.Code(err) != codes.NotFound { - logger.WithError(err).Warn("Failed to get LoRa Packet Forwarder profile") - return nil, err - } - res, err := loraPFProfileClient.Create(ctx, &northboundv1.LoraPacketForwarderProfileServiceCreateRequest{ - Domain: u.client.Domain(ctx), - Group: profileGroup, - LoraPacketForwarderProfile: loraPFProfile, - }) - if err != nil { - logger.WithError(err).Warn("Failed to create LoRa Packet Forwarder profile") - return nil, err - } - loraPFProfileID = res.ProfileId - } else { - if profile := loraPFGetRes.LoraPacketForwarderProfile; profile.Shared != loraPFProfile.Shared || - profile.Protocol != loraPFProfile.Protocol || - !bytes.Equal(profile.RootCa, loraPFProfile.RootCa) { - _, err := loraPFProfileClient.Update(ctx, &northboundv1.LoraPacketForwarderProfileServiceUpdateRequest{ - Domain: u.client.Domain(ctx), - Group: profileGroup, - ProfileId: loraPFGetRes.ProfileId, - LoraPacketForwarderProfile: loraPFProfile, - }) - if err != nil { - logger.WithError(err).Warn("Failed to update LoRa Packet Forwarder profile") - return nil, err - } - } - loraPFProfileID = loraPFGetRes.ProfileId + if u.supportsProtocol(desc, northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_TTI_V1) { + return u.claimTTIV1Gateway(ctx, eui, ownerToken, clusterAddress) } - // Upsert the Geolocation profile. - var ( - geolocationProfileID []byte - geolocationProfile = &northboundv1.GeolocationProfile{ - ProfileName: "on connect", - Shared: true, - DisconnectedFor: durationpb.New(0), - } - geolocationProfileClient = northboundv1.NewGeolocationProfileServiceClient(u.client) - ) - geolocationGetRes, err := geolocationProfileClient.GetByName( - ctx, - &northboundv1.GeolocationProfileServiceGetByNameRequest{ - Domain: u.client.Domain(ctx), - Group: profileGroup, - ProfileName: geolocationProfile.ProfileName, - }, - ) - if err != nil { - if status.Code(err) != codes.NotFound { - logger.WithError(err).Warn("Failed to get geolocation profile") - return nil, err - } - res, err := geolocationProfileClient.Create(ctx, &northboundv1.GeolocationProfileServiceCreateRequest{ - Domain: u.client.Domain(ctx), - Group: profileGroup, - GeolocationProfile: geolocationProfile, - }) - if err != nil { - logger.WithError(err).Warn("Failed to create geolocation profile") - return nil, err - } - geolocationProfileID = res.ProfileId - } else { - geolocationProfileID = geolocationGetRes.ProfileId + if u.supportsProtocol(desc, northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_LBS_CUPS) { + return u.claimLBSCUPSGateway(ctx, eui, ownerToken, clusterAddress) } - // Update the gateway with the profiles. - _, err = gtwClient.Update(ctx, &northboundv1.GatewayServiceUpdateRequest{ - GatewayId: eui.MarshalNumber(), - Domain: u.client.Domain(ctx), - LoraPacketForwarderProfileId: &northboundv1.ProfileIDValue{ - Value: loraPFProfileID, - }, - GeolocationProfileId: &northboundv1.ProfileIDValue{ - Value: geolocationProfileID, - }, - }) - if err != nil { - logger.WithError(err).Warn("Failed to update gateway with profiles") - return nil, err - } + return nil, errNoSupportedProtocol.New() +} - gatewayMetadata := &dcstypes.GatewayMetadata{} - locationRes, err := gtwClient.GetLastLocation(ctx, &northboundv1.GatewayServiceGetLastLocationRequest{ - GatewayId: eui.MarshalNumber(), - Domain: u.client.Domain(ctx), - }) - if err != nil && !errors.IsNotFound(err) { - logger.WithError(err).Warn("Failed to get gateway location") - } else if err == nil { - gatewayMetadata.Antennas = []*ttnpb.GatewayAntenna{ - { - Location: &ttnpb.Location{ - Latitude: locationRes.Location.Latitude, - Longitude: locationRes.Location.Longitude, - Accuracy: int32(locationRes.Location.Accuracy), - }, - }, +func (*Upstream) supportsProtocol( + desc *northboundv1.GatewayServiceDescribeResponse, + protocolID northboundv1.GatewayProtocolIdentifier, +) bool { + for _, p := range desc.SupportedGatewayProtocols { + if p.GatewayProtocolId == protocolID { + return true } } - return gatewayMetadata, nil + return false } // Unclaim implements gateways.GatewayClaimer. func (u *Upstream) Unclaim(ctx context.Context, eui types.EUI64) error { + // Delete the CUPS and LNS API keys for the gateway. + if err := u.deleteAPIKeys(ctx, &ttnpb.GatewayIdentifiers{Eui: eui.Bytes()}); err != nil { + // Don't fail unclaiming if deleting the API keys fails. + log.FromContext(ctx).WithError(err).Warn("Failed to delete API keys for gateway") + } + gtwClient := northboundv1.NewGatewayServiceClient(u.client) _, err := gtwClient.Unclaim(ctx, &northboundv1.GatewayServiceUnclaimRequest{ GatewayId: eui.MarshalNumber(), diff --git a/pkg/deviceclaimingserver/gateways/ttgc/ttiv1.go b/pkg/deviceclaimingserver/gateways/ttgc/ttiv1.go new file mode 100644 index 0000000000..12d32b965c --- /dev/null +++ b/pkg/deviceclaimingserver/gateways/ttgc/ttiv1.go @@ -0,0 +1,189 @@ +// Copyright © 2026 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ttgc + +import ( + "bytes" + "context" + "net" + + northboundv1 "go.thethings.industries/pkg/api/gen/tti/gateway/controller/northbound/v1" + dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/log" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" +) + +// claimTTIV1Gateway does four things: +// 1. Claim the gateway +// 2. Upsert a LoRa Packet Forwarder profile with the root CA presented by the given Gateway Server +// 3. Upsert a Geolocation profile +// 4. Update the gateway with the profiles +func (u *Upstream) claimTTIV1Gateway( + ctx context.Context, eui types.EUI64, ownerToken, clusterAddress string, +) (*dcstypes.GatewayMetadata, error) { + logger := log.FromContext(ctx) + + // Claim the gateway. + gtwClient := northboundv1.NewGatewayServiceClient(u.client) + _, err := gtwClient.Claim(ctx, &northboundv1.GatewayServiceClaimRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + OwnerToken: ownerToken, + }) + if err != nil { + return nil, err + } + + // Get the root CA from the Gateway Server and upsert the LoRa Packet Forwarder profile. + host, _, err := net.SplitHostPort(clusterAddress) + if err != nil { + host = clusterAddress + } + clusterAddress = net.JoinHostPort(host, "8889") + rootCA, err := u.getRootCA(ctx, clusterAddress) + if err != nil { + return nil, err + } + var ( + loraPFProfileID []byte + loraPFProfile = &northboundv1.LoraPacketForwarderProfile{ + ProfileName: clusterAddress, + Shared: true, + Protocol: northboundv1.LoraPacketForwarderProtocol_LORA_PACKET_FORWARDER_PROTOCOL_TTI_V1, + Address: clusterAddress, + RootCa: rootCA.Raw, + } + loraPFProfileClient = northboundv1.NewLoraPacketForwarderProfileServiceClient(u.client) + ) + loraPFGetRes, err := loraPFProfileClient.GetByName( + ctx, + &northboundv1.LoraPacketForwarderProfileServiceGetByNameRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + ProfileName: clusterAddress, + }, + ) + if err != nil { + if status.Code(err) != codes.NotFound { + logger.WithError(err).Warn("Failed to get LoRa Packet Forwarder profile") + return nil, err + } + res, err := loraPFProfileClient.Create(ctx, &northboundv1.LoraPacketForwarderProfileServiceCreateRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + LoraPacketForwarderProfile: loraPFProfile, + }) + if err != nil { + logger.WithError(err).Warn("Failed to create LoRa Packet Forwarder profile") + return nil, err + } + loraPFProfileID = res.ProfileId + } else { + if profile := loraPFGetRes.LoraPacketForwarderProfile; profile.Shared != loraPFProfile.Shared || + profile.Protocol != loraPFProfile.Protocol || + !bytes.Equal(profile.RootCa, loraPFProfile.RootCa) { + _, err := loraPFProfileClient.Update(ctx, &northboundv1.LoraPacketForwarderProfileServiceUpdateRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + ProfileId: loraPFGetRes.ProfileId, + LoraPacketForwarderProfile: loraPFProfile, + }) + if err != nil { + logger.WithError(err).Warn("Failed to update LoRa Packet Forwarder profile") + return nil, err + } + } + loraPFProfileID = loraPFGetRes.ProfileId + } + + // Upsert the Geolocation profile. + var ( + geolocationProfileID []byte + geolocationProfile = &northboundv1.GeolocationProfile{ + ProfileName: "on connect", + Shared: true, + DisconnectedFor: durationpb.New(0), + } + geolocationProfileClient = northboundv1.NewGeolocationProfileServiceClient(u.client) + ) + geolocationGetRes, err := geolocationProfileClient.GetByName( + ctx, + &northboundv1.GeolocationProfileServiceGetByNameRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + ProfileName: geolocationProfile.ProfileName, + }, + ) + if err != nil { + if status.Code(err) != codes.NotFound { + logger.WithError(err).Warn("Failed to get geolocation profile") + return nil, err + } + res, err := geolocationProfileClient.Create(ctx, &northboundv1.GeolocationProfileServiceCreateRequest{ + Domain: u.client.Domain(ctx), + Group: profileGroup, + GeolocationProfile: geolocationProfile, + }) + if err != nil { + logger.WithError(err).Warn("Failed to create geolocation profile") + return nil, err + } + geolocationProfileID = res.ProfileId + } else { + geolocationProfileID = geolocationGetRes.ProfileId + } + + // Update the gateway with the profiles. + _, err = gtwClient.Update(ctx, &northboundv1.GatewayServiceUpdateRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + LoraPacketForwarderProfileId: &northboundv1.ProfileIDValue{ + Value: loraPFProfileID, + }, + GeolocationProfileId: &northboundv1.ProfileIDValue{ + Value: geolocationProfileID, + }, + }) + if err != nil { + logger.WithError(err).Warn("Failed to update gateway with profiles") + return nil, err + } + + gatewayMetadata := &dcstypes.GatewayMetadata{} + locationRes, err := gtwClient.GetLastLocation(ctx, &northboundv1.GatewayServiceGetLastLocationRequest{ + GatewayId: eui.MarshalNumber(), + Domain: u.client.Domain(ctx), + }) + if err != nil && !errors.IsNotFound(err) { + logger.WithError(err).Warn("Failed to get gateway location") + } else if err == nil { + gatewayMetadata.Antennas = []*ttnpb.GatewayAntenna{ + { + Location: &ttnpb.Location{ + Latitude: locationRes.Location.Latitude, + Longitude: locationRes.Location.Longitude, + Accuracy: int32(locationRes.Location.Accuracy), + }, + }, + } + } + + return gatewayMetadata, nil +} diff --git a/pkg/ttgc/config.go b/pkg/ttgc/config.go index f32188129f..4e1dced72d 100644 --- a/pkg/ttgc/config.go +++ b/pkg/ttgc/config.go @@ -21,11 +21,9 @@ import ( // Config is the configuration for The Things Gateway Controller. type Config struct { - Enabled bool `name:"enabled" description:"Enable The Things Gateway Controller"` - GatewayEUIs []types.EUI64Prefix `name:"gateway-euis" description:"Gateway EUI prefixes that are managed by The Things Gateway Controller"` //nolint:lll - LBSCUPSEnabled bool `name:"lbs-cups-enabled" description:"Enable LBS CUPS protocol for gateway claiming"` - LBSCUPSGatewayEUIs []types.EUI64Prefix `name:"lbs-cups-gateway-euis" description:"Gateway EUI prefixes that are managed by LBS CUPS protocol"` //nolint:lll - Address string `name:"address" description:"The address of The Things Gateway Controller"` - Domain string `name:"domain" description:"The domain of this cluster"` - TLS tlsconfig.ClientAuth `name:"tls" description:"TLS configuration"` + Enabled bool `name:"enabled" description:"Enable The Things Gateway Controller"` + GatewayEUIs []types.EUI64Prefix `name:"gateway-euis" description:"Gateway EUI prefixes that are managed by The Things Gateway Controller"` //nolint:lll + Address string `name:"address" description:"The address of The Things Gateway Controller"` + Domain string `name:"domain" description:"The domain of this cluster"` + TLS tlsconfig.ClientAuth `name:"tls" description:"TLS configuration"` } From ae7f43afb1139cb4ad30e54017b733f937219da3 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:18:18 +0200 Subject: [PATCH 08/10] dcs: Prioritise the claiming option for ttgc managed gateways --- .../gateways/ttgc/ttgc.go | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go index e5a76d849f..34038982b1 100644 --- a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go +++ b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go @@ -32,8 +32,10 @@ import ( ) var ( - errNoSupportedProtocol = errors.DefineFailedPrecondition("no_supported_protocol", "no supported gateway protocol found for claiming") - errNoSupportedAuthMethod = errors.DefineFailedPrecondition("no_supported_auth_method", "no supported authentication method found for gateway") + errNoSupportedClaimOption = errors.DefineFailedPrecondition( + "no_supported_claim_option", + "no supported claim option (protocol + auth method) found for gateway", + ) ) const profileGroup = "tts" @@ -65,10 +67,18 @@ func New(ctx context.Context, c component, config ttgc.Config) (*Upstream, error }, nil } +// claimOption represents the protocol and authentication method for claiming a gateway. +type claimOption struct { + protocol northboundv1.GatewayProtocolIdentifier + authMethod northboundv1.AuthenticationMethod + handler func(context.Context, types.EUI64, string, string) (*dcstypes.GatewayMetadata, error) +} + // Claim implements gateways.GatewayClaimer. func (u *Upstream) Claim( ctx context.Context, eui types.EUI64, ownerToken, clusterAddress string, ) (*dcstypes.GatewayMetadata, error) { + // Get the gateway description to verify what protocol it supports. gtwClient := northboundv1.NewGatewayServiceClient(u.client) desc, err := gtwClient.Describe(ctx, &northboundv1.GatewayServiceDescribeRequest{ @@ -78,24 +88,42 @@ func (u *Upstream) Claim( return nil, err } - if u.supportsProtocol(desc, northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_TTI_V1) { - return u.claimTTIV1Gateway(ctx, eui, ownerToken, clusterAddress) + // Defines the preferred claiming options in order. + var claimPreferences = []claimOption{ + { + protocol: northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_TTI_V1, + authMethod: northboundv1.AuthenticationMethod_AUTHENTICATION_METHOD_MUTUAL_TLS, + handler: u.claimTTIV1Gateway, + }, + { + protocol: northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_LBS_CUPS, + authMethod: northboundv1.AuthenticationMethod_AUTHENTICATION_METHOD_GATEWAY_TOKEN, + handler: u.claimLBSCUPSGateway, + }, } - if u.supportsProtocol(desc, northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_LBS_CUPS) { - return u.claimLBSCUPSGateway(ctx, eui, ownerToken, clusterAddress) + // Select the first supported claiming option and use its handler. + for _, option := range claimPreferences { + if u.supportsOption(desc, option) { + return option.handler(ctx, eui, ownerToken, clusterAddress) + } } - return nil, errNoSupportedProtocol.New() + return nil, errNoSupportedClaimOption.New() } -func (*Upstream) supportsProtocol( +func (u *Upstream) supportsOption( desc *northboundv1.GatewayServiceDescribeResponse, - protocolID northboundv1.GatewayProtocolIdentifier, + option claimOption, ) bool { for _, p := range desc.SupportedGatewayProtocols { - if p.GatewayProtocolId == protocolID { - return true + if p.GatewayProtocolId != option.protocol { + continue + } + for _, a := range p.SupportedAuthenticationMethods { + if a == option.authMethod { + return true + } } } From e63bef2355a6a0c970fb17f12cf6dc8031e6e47e Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:12:40 +0200 Subject: [PATCH 09/10] dev: Update gateway controller API & dependencies --- config/messages.json | 27 +++++++++ go.mod | 28 ++++----- go.sum | 52 ++++++++-------- .../gateways/ttgc/ttgc.go | 24 ++++---- pkg/webui/locales/ja.json | 3 + tools/go.mod | 32 +++++----- tools/go.sum | 60 +++++++++---------- 7 files changed, 126 insertions(+), 100 deletions(-) diff --git a/config/messages.json b/config/messages.json index 88d53d65fb..8baff606a9 100644 --- a/config/messages.json +++ b/config/messages.json @@ -4013,6 +4013,24 @@ "file": "ttjs.go" } }, + "error:pkg/deviceclaimingserver/gateways/ttgc:create_api_key": { + "translations": { + "en": "failed to create API key for gateway" + }, + "description": { + "package": "pkg/deviceclaimingserver/gateways/ttgc", + "file": "lbscups.go" + } + }, + "error:pkg/deviceclaimingserver/gateways/ttgc:delete_api_key": { + "translations": { + "en": "delete API key" + }, + "description": { + "package": "pkg/deviceclaimingserver/gateways/ttgc", + "file": "lbscups.go" + } + }, "error:pkg/deviceclaimingserver/gateways/ttgc:dial_gateway_server": { "translations": { "en": "dial Gateway Gerver `{address}`" @@ -4022,6 +4040,15 @@ "file": "root_ca.go" } }, + "error:pkg/deviceclaimingserver/gateways/ttgc:no_supported_claim_option": { + "translations": { + "en": "no supported claim option (protocol + auth method) found for gateway" + }, + "description": { + "package": "pkg/deviceclaimingserver/gateways/ttgc", + "file": "ttgc.go" + } + }, "error:pkg/deviceclaimingserver/gateways/ttgc:verify_gateway_server_tls": { "translations": { "en": "verify TLS server certificate of Gateway Server `{address}`" diff --git a/go.mod b/go.mod index 614cc42ad3..561a5c28b9 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 github.com/hellofresh/health-go/v5 v5.5.5 github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef github.com/iancoleman/strcase v0.3.0 @@ -100,7 +100,7 @@ require ( go.packetbroker.org/api/mapping/v2 v2.3.2 go.packetbroker.org/api/routing v1.9.2 go.packetbroker.org/api/v3 v3.17.1 - go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20241212200202-1050b2b3ffa6 + go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20260326103632-2a98d7e48bc4 go.thethings.industries/pkg/ca v0.0.0-20241212200202-1050b2b3ffa6 go.thethings.network/lorawan-application-payload v0.0.0-20220125153912-1198ff1e403e go.thethings.network/lorawan-stack-legacy/v2 v2.1.0 @@ -108,23 +108,23 @@ require ( go.uber.org/zap v1.27.0 gocloud.dev v0.42.0 gocloud.dev/pubsub/natspubsub v0.42.0 - golang.org/x/crypto v0.40.0 + golang.org/x/crypto v0.46.0 golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc - golang.org/x/net v0.42.0 - golang.org/x/oauth2 v0.30.0 - golang.org/x/sync v0.16.0 + golang.org/x/net v0.48.0 + golang.org/x/oauth2 v0.35.0 + golang.org/x/sync v0.19.0 google.golang.org/genproto v0.0.0-20250707201910-8d1bb00bc6a7 - google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 - google.golang.org/grpc v1.73.0 - google.golang.org/protobuf v1.36.6 + google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c + google.golang.org/grpc v1.79.1 + google.golang.org/protobuf v1.36.11 gopkg.in/mail.v2 v2.3.1 gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.121.3 // indirect cloud.google.com/go/auth v0.16.2 // indirect @@ -261,9 +261,9 @@ require ( go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/image v0.29.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/term v0.33.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.241.0 // indirect diff --git a/go.sum b/go.sum index 3baee78205..741eac65f6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1 h1:6tCo3lsKNLqUjRPhyc8JuYWYUiQkulufxSDOfG1zgWQ= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= @@ -1257,8 +1257,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -1660,8 +1660,8 @@ go.packetbroker.org/api/routing v1.9.2 h1:J4+4vYZxa60UWC70Y9yy7sktU7DXaAp9Q13Bfq go.packetbroker.org/api/routing v1.9.2/go.mod h1:kd2K7gieDI35YfPA8/zDmLX3qiKPuXia/MA77BEAeUA= go.packetbroker.org/api/v3 v3.17.1 h1:LcyFPUGqVubGWMvQ16tZlQIKd+noGx7urzEYhSLiEQA= go.packetbroker.org/api/v3 v3.17.1/go.mod h1:6bVbdWAYLnvZ5kgXxA7GBQvZTN7vxI0DoF1Di1NoAT4= -go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20241212200202-1050b2b3ffa6 h1:7JPZg5V2amhtut3eGrb0jjYIagxXcDNCxDf2s7cQm4E= -go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20241212200202-1050b2b3ffa6/go.mod h1:j7/Mx4U9xAMR8tQysnuDtYWJLpSmOOj9Ljpp/2zjnbs= +go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20260326103632-2a98d7e48bc4 h1:GIa/51gzz44L5sJ+U2M+988Tf0ytrsmzsEPg9FnAPlY= +go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20260326103632-2a98d7e48bc4/go.mod h1:3ZpxzNmUSjtyzXOi4my7E9s6IhThg3jkFSpn9i+PAI8= go.thethings.industries/pkg/ca v0.0.0-20241212200202-1050b2b3ffa6 h1:cZfYKkQTmIftluI02J/gsbtxHj3mqmbXkJQTRzGTHTM= go.thethings.industries/pkg/ca v0.0.0-20241212200202-1050b2b3ffa6/go.mod h1:r258GXhMCjAhBVvJxd5AVmxuoiHZ/fSxY76WQpDwSB0= go.thethings.network/lorawan-application-payload v0.0.0-20220125153912-1198ff1e403e h1:TWGQ3lh7gI2W5hnb6qPdpoAa0d7s/XPwvgf2VVCMJaY= @@ -1708,8 +1708,8 @@ golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5D golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1847,8 +1847,8 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1880,8 +1880,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1905,8 +1905,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2005,8 +2005,8 @@ golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -2027,8 +2027,8 @@ golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2051,8 +2051,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2373,8 +2373,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go. google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= @@ -2393,8 +2393,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -2423,8 +2423,8 @@ google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go index 34038982b1..db42b96bfe 100644 --- a/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go +++ b/pkg/deviceclaimingserver/gateways/ttgc/ttgc.go @@ -18,6 +18,7 @@ package ttgc import ( "context" "crypto/tls" + "slices" northboundv1 "go.thethings.industries/pkg/api/gen/tti/gateway/controller/northbound/v1" "go.thethings.network/lorawan-stack/v3/pkg/cluster" @@ -31,11 +32,9 @@ import ( "google.golang.org/grpc" ) -var ( - errNoSupportedClaimOption = errors.DefineFailedPrecondition( - "no_supported_claim_option", - "no supported claim option (protocol + auth method) found for gateway", - ) +var errNoSupportedClaimOption = errors.DefineFailedPrecondition( + "no_supported_claim_option", + "no supported claim option (protocol + auth method) found for gateway", ) const profileGroup = "tts" @@ -78,7 +77,6 @@ type claimOption struct { func (u *Upstream) Claim( ctx context.Context, eui types.EUI64, ownerToken, clusterAddress string, ) (*dcstypes.GatewayMetadata, error) { - // Get the gateway description to verify what protocol it supports. gtwClient := northboundv1.NewGatewayServiceClient(u.client) desc, err := gtwClient.Describe(ctx, &northboundv1.GatewayServiceDescribeRequest{ @@ -89,14 +87,14 @@ func (u *Upstream) Claim( } // Defines the preferred claiming options in order. - var claimPreferences = []claimOption{ + claimPreferences := []claimOption{ { - protocol: northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_TTI_V1, + protocol: northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_IDENTIFIER_TTI_V1, authMethod: northboundv1.AuthenticationMethod_AUTHENTICATION_METHOD_MUTUAL_TLS, handler: u.claimTTIV1Gateway, }, { - protocol: northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_LBS_CUPS, + protocol: northboundv1.GatewayProtocolIdentifier_GATEWAY_PROTOCOL_IDENTIFIER_LBS_LNS, authMethod: northboundv1.AuthenticationMethod_AUTHENTICATION_METHOD_GATEWAY_TOKEN, handler: u.claimLBSCUPSGateway, }, @@ -112,7 +110,7 @@ func (u *Upstream) Claim( return nil, errNoSupportedClaimOption.New() } -func (u *Upstream) supportsOption( +func (*Upstream) supportsOption( desc *northboundv1.GatewayServiceDescribeResponse, option claimOption, ) bool { @@ -120,10 +118,8 @@ func (u *Upstream) supportsOption( if p.GatewayProtocolId != option.protocol { continue } - for _, a := range p.SupportedAuthenticationMethods { - if a == option.authMethod { - return true - } + if slices.Contains(p.SupportedAuthenticationMethods, option.authMethod) { + return true } } diff --git a/pkg/webui/locales/ja.json b/pkg/webui/locales/ja.json index dc92705558..664d196ca0 100644 --- a/pkg/webui/locales/ja.json +++ b/pkg/webui/locales/ja.json @@ -2334,7 +2334,10 @@ "error:pkg/deviceclaimingserver/enddevices/ttjsv2:internal_error": "内部エラー", "error:pkg/deviceclaimingserver/enddevices/ttjsv2:unclaim_device": "EUI `{dev_eui}`のデバイスの主張を取り消す", "error:pkg/deviceclaimingserver/enddevices/ttjsv2:unclaim_devices": "デバイスの主張を取り消す", + "error:pkg/deviceclaimingserver/gateways/ttgc:create_api_key": "", + "error:pkg/deviceclaimingserver/gateways/ttgc:delete_api_key": "", "error:pkg/deviceclaimingserver/gateways/ttgc:dial_gateway_server": "", + "error:pkg/deviceclaimingserver/gateways/ttgc:no_supported_claim_option": "", "error:pkg/deviceclaimingserver/gateways/ttgc:verify_gateway_server_tls": "", "error:pkg/deviceclaimingserver/gateways:invalid_upstream": "", "error:pkg/deviceclaimingserver/gateways:ttgc_not_enabled": "", diff --git a/tools/go.mod b/tools/go.mod index f109efde1a..be16d53ae5 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -20,7 +20,7 @@ require ( ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.121.3 // indirect cloud.google.com/go/auth v0.16.2 // indirect @@ -130,7 +130,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hellofresh/health-go/v5 v5.5.5 // indirect github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef // indirect @@ -227,7 +227,7 @@ require ( go.packetbroker.org/api/mapping/v2 v2.3.2 // indirect go.packetbroker.org/api/routing v1.9.2 // indirect go.packetbroker.org/api/v3 v3.17.1 // indirect - go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20241212200202-1050b2b3ffa6 // indirect + go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20260326103632-2a98d7e48bc4 // indirect go.thethings.industries/pkg/ca v0.0.0-20241212200202-1050b2b3ffa6 // indirect go.thethings.network/lorawan-application-payload v0.0.0-20220125153912-1198ff1e403e // indirect go.thethings.network/lorawan-stack-legacy/v2 v2.1.0 // indirect @@ -236,25 +236,25 @@ require ( go.uber.org/zap v1.27.0 // indirect gocloud.dev v0.42.0 // indirect gocloud.dev/pubsub/natspubsub v0.42.0 // indirect - golang.org/x/crypto v0.40.0 // indirect + golang.org/x/crypto v0.47.0 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect golang.org/x/image v0.29.0 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/term v0.33.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.35.0 // indirect + golang.org/x/tools v0.41.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.241.0 // indirect google.golang.org/genproto v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/grpc v1.73.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/grpc v1.79.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index 11e1bc62e6..6668918d71 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1,5 +1,5 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1 h1:6tCo3lsKNLqUjRPhyc8JuYWYUiQkulufxSDOfG1zgWQ= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250625184727-c923a0c2a132.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= @@ -1265,8 +1265,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -1704,8 +1704,8 @@ go.packetbroker.org/api/routing v1.9.2 h1:J4+4vYZxa60UWC70Y9yy7sktU7DXaAp9Q13Bfq go.packetbroker.org/api/routing v1.9.2/go.mod h1:kd2K7gieDI35YfPA8/zDmLX3qiKPuXia/MA77BEAeUA= go.packetbroker.org/api/v3 v3.17.1 h1:LcyFPUGqVubGWMvQ16tZlQIKd+noGx7urzEYhSLiEQA= go.packetbroker.org/api/v3 v3.17.1/go.mod h1:6bVbdWAYLnvZ5kgXxA7GBQvZTN7vxI0DoF1Di1NoAT4= -go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20241212200202-1050b2b3ffa6 h1:7JPZg5V2amhtut3eGrb0jjYIagxXcDNCxDf2s7cQm4E= -go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20241212200202-1050b2b3ffa6/go.mod h1:j7/Mx4U9xAMR8tQysnuDtYWJLpSmOOj9Ljpp/2zjnbs= +go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20260326103632-2a98d7e48bc4 h1:GIa/51gzz44L5sJ+U2M+988Tf0ytrsmzsEPg9FnAPlY= +go.thethings.industries/pkg/api/gen/tti/gateway v0.0.0-20260326103632-2a98d7e48bc4/go.mod h1:3ZpxzNmUSjtyzXOi4my7E9s6IhThg3jkFSpn9i+PAI8= go.thethings.industries/pkg/ca v0.0.0-20241212200202-1050b2b3ffa6 h1:cZfYKkQTmIftluI02J/gsbtxHj3mqmbXkJQTRzGTHTM= go.thethings.industries/pkg/ca v0.0.0-20241212200202-1050b2b3ffa6/go.mod h1:r258GXhMCjAhBVvJxd5AVmxuoiHZ/fSxY76WQpDwSB0= go.thethings.network/lorawan-application-payload v0.0.0-20220125153912-1198ff1e403e h1:TWGQ3lh7gI2W5hnb6qPdpoAa0d7s/XPwvgf2VVCMJaY= @@ -1755,8 +1755,8 @@ golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5D golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1824,8 +1824,8 @@ golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1896,8 +1896,8 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1929,8 +1929,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1954,8 +1954,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2058,8 +2058,8 @@ golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -2081,8 +2081,8 @@ golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2105,8 +2105,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -2185,8 +2185,8 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2430,8 +2430,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go. google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY= google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= @@ -2450,8 +2450,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -2480,8 +2480,8 @@ google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 3550b8626a47702564f7aaac575467f982832622 Mon Sep 17 00:00:00 2001 From: Vlad Vitan <23100181+vlasebian@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:22:27 +0200 Subject: [PATCH 10/10] dev: Fix lint errors --- pkg/deviceclaimingserver/grpc_gateways_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/deviceclaimingserver/grpc_gateways_test.go b/pkg/deviceclaimingserver/grpc_gateways_test.go index 786acbd47a..0bc10e5362 100644 --- a/pkg/deviceclaimingserver/grpc_gateways_test.go +++ b/pkg/deviceclaimingserver/grpc_gateways_test.go @@ -258,7 +258,7 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest TargetGatewayServerAddress: "things.example.com", }, CallOpt: authorizedCallOpt, - CreateFunc: func(_ context.Context, in *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { + CreateFunc: func(_ context.Context, _ *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { return nil, errCreate.New() }, ErrorAssertion: errors.IsAborted, @@ -331,9 +331,9 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest return &dcstypes.GatewayMetadata{}, nil }, CreateFunc: func(context.Context, *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { - return nil, nil + return nil, nil //nolint:nilnil }, - UpdateFunc: func(_ context.Context, in *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { + UpdateFunc: func(_ context.Context, _ *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { return nil, errUpdate.New() }, DeleteFunc: func(_ context.Context, _ *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) { @@ -365,9 +365,9 @@ func TestGatewayClaimingServer(t *testing.T) { //nolint:paralleltest return &dcstypes.GatewayMetadata{}, nil }, CreateFunc: func(context.Context, *ttnpb.CreateGatewayRequest) (*ttnpb.Gateway, error) { - return nil, nil + return nil, nil //nolint:nilnil }, - UpdateFunc: func(_ context.Context, in *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { + UpdateFunc: func(_ context.Context, _ *ttnpb.UpdateGatewayRequest) (*ttnpb.Gateway, error) { return nil, errUpdate.New() }, DeleteFunc: func(_ context.Context, _ *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) {