Skip to content

Commit 2e93568

Browse files
committed
dcs: Implement TTGC LBS CUPS claimer
1 parent a3670ab commit 2e93568

4 files changed

Lines changed: 410 additions & 7 deletions

File tree

pkg/deviceclaimingserver/gateways/gateways.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,25 @@ import (
2020
"crypto/tls"
2121
"strings"
2222

23+
"go.thethings.network/lorawan-stack/v3/pkg/cluster"
2324
"go.thethings.network/lorawan-stack/v3/pkg/config"
2425
"go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig"
26+
"go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways/lbscups"
2527
"go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/gateways/ttgc"
2628
dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types"
2729
"go.thethings.network/lorawan-stack/v3/pkg/errors"
30+
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
2831
"go.thethings.network/lorawan-stack/v3/pkg/types"
32+
"google.golang.org/grpc"
2933
)
3034

3135
// Component is the interface to the component.
3236
type Component interface {
3337
GetBaseConfig(context.Context) config.ServiceBase
3438
GetTLSConfig(context.Context) tlsconfig.Config
3539
GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error)
40+
GetPeerConn(ctx context.Context, role ttnpb.ClusterRole, ids cluster.EntityIdentifiers) (*grpc.ClientConn, error)
41+
AllowInsecureForCredentials() bool
3642
}
3743

3844
// Config is the configuration for the Gateway Claiming Server.
@@ -43,8 +49,9 @@ type Config struct {
4349
}
4450

4551
var (
46-
errInvalidUpstream = errors.DefineInvalidArgument("invalid_upstream", "upstream `{name}` is invalid")
47-
errTTGCNotEnabled = errors.DefineFailedPrecondition("ttgc_not_enabled", "TTGC is not enabled")
52+
errInvalidUpstream = errors.DefineInvalidArgument("invalid_upstream", "upstream `{name}` is invalid")
53+
errTTGCNotEnabled = errors.DefineFailedPrecondition("ttgc_not_enabled", "TTGC is not enabled")
54+
errLBSCUPSNotEnabled = errors.DefineFailedPrecondition("lbs_cups_not_enabled", "TTGC LBS CUPS is not enabled")
4855
)
4956

5057
// ParseGatewayEUIRanges parses the configured upstream map and returns map of ranges.
@@ -134,6 +141,13 @@ func NewUpstream(
134141
}
135142
hosts["ttgc"] = ttgcRanges
136143
}
144+
if _, lbscupsAdded := hosts["lbs-cups"]; ttgcConf.LBSCUPSEnabled && !lbscupsAdded {
145+
lbscupsRanges := make([]dcstypes.EUI64Range, len(ttgcConf.LBSCUPSGatewayEUIs))
146+
for i, prefix := range ttgcConf.LBSCUPSGatewayEUIs {
147+
lbscupsRanges[i] = dcstypes.RangeFromEUI64Prefix(prefix)
148+
}
149+
hosts["lbs-cups"] = lbscupsRanges
150+
}
137151

138152
// Setup upstream table.
139153
for name, ranges := range hosts {
@@ -150,6 +164,14 @@ func NewUpstream(
150164
if err != nil {
151165
return nil, err
152166
}
167+
case "lbs-cups":
168+
if !ttgcConf.LBSCUPSEnabled {
169+
return nil, errLBSCUPSNotEnabled.New()
170+
}
171+
claimer, err = lbscups.New(ctx, c, ttgcConf)
172+
if err != nil {
173+
return nil, err
174+
}
153175
default:
154176
return nil, errInvalidUpstream.WithAttributes("name", name)
155177
}
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
// Copyright © 2024 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package lbscups provides functions to claim gateways using LBS CUPS protocol.
16+
package lbscups
17+
18+
import (
19+
"bytes"
20+
"context"
21+
"crypto/tls"
22+
"fmt"
23+
"net"
24+
"time"
25+
26+
northboundv1 "go.thethings.industries/pkg/api/gen/tti/gateway/controller/northbound/v1"
27+
"go.thethings.network/lorawan-stack/v3/pkg/cluster"
28+
"go.thethings.network/lorawan-stack/v3/pkg/config/tlsconfig"
29+
dcstypes "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/types"
30+
"go.thethings.network/lorawan-stack/v3/pkg/errors"
31+
"go.thethings.network/lorawan-stack/v3/pkg/log"
32+
"go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata"
33+
"go.thethings.network/lorawan-stack/v3/pkg/ttgc"
34+
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
35+
"go.thethings.network/lorawan-stack/v3/pkg/types"
36+
"google.golang.org/grpc"
37+
"google.golang.org/grpc/codes"
38+
"google.golang.org/grpc/status"
39+
)
40+
41+
const profileGroup = "tts"
42+
43+
type component interface {
44+
GetTLSConfig(context.Context) tlsconfig.Config
45+
GetTLSClientConfig(context.Context, ...tlsconfig.Option) (*tls.Config, error)
46+
GetPeerConn(ctx context.Context, role ttnpb.ClusterRole, ids cluster.EntityIdentifiers) (*grpc.ClientConn, error)
47+
AllowInsecureForCredentials() bool
48+
}
49+
50+
// Upstream is the client for LBS CUPS gateway claiming.
51+
type Upstream struct {
52+
component
53+
client *ttgc.Client
54+
55+
gatewayAccess ttnpb.GatewayAccessClient
56+
}
57+
58+
// New returns a new upstream client for LBS CUPS gateway claiming.
59+
func New(ctx context.Context, c component, config ttgc.Config) (*Upstream, error) {
60+
client, err := ttgc.NewClient(ctx, c, config)
61+
if err != nil {
62+
return nil, err
63+
}
64+
upstream := &Upstream{
65+
component: c,
66+
client: client,
67+
}
68+
return upstream, nil
69+
}
70+
71+
var errCreateAPIKey = errors.DefineAborted("create_api_key", "create API key")
72+
73+
// Claim implements gateways.Claimer.
74+
// Claim does the following:
75+
// 1. Create CUPS and LNS API keys for the gateway
76+
// 2. Claim the gateway on TTGC with the CUPS key as the gateway token
77+
// 3. Return the LNS key in GatewayMetadata
78+
func (u *Upstream) Claim(
79+
ctx context.Context, eui types.EUI64, ownerToken, clusterAddress string,
80+
) (*dcstypes.GatewayMetadata, error) {
81+
logger := log.FromContext(ctx)
82+
83+
ids := &ttnpb.GatewayIdentifiers{
84+
Eui: eui.Bytes(),
85+
}
86+
87+
// Create CUPS and LNS API keys for the gateway. The CUPS key will be used as gateway token when claiming on TTGC and
88+
// the LNS key will be returned in the metadata. The caller is responsible for updating the LNS key in the gateway.
89+
cupsKey, lnsKey, err := u.createAPIKeys(ctx, ids)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// Claim the gateway on TTGC with the CUPS key as the gateway token.
95+
gtwClient := northboundv1.NewGatewayServiceClient(u.client)
96+
_, err = gtwClient.Claim(ctx, &northboundv1.GatewayServiceClaimRequest{
97+
GatewayId: eui.MarshalNumber(),
98+
Domain: u.client.Domain(ctx),
99+
OwnerToken: ownerToken,
100+
GatewayToken: []byte(cupsKey.Key),
101+
})
102+
if err != nil {
103+
logger.WithError(err).Warn("Failed to claim gateway on TTGC")
104+
return nil, err
105+
}
106+
107+
// Get the Root CA from the Gateway Server.
108+
host, _, err := net.SplitHostPort(clusterAddress)
109+
if err != nil {
110+
host = clusterAddress
111+
}
112+
clusterAddress = net.JoinHostPort(host, "8889")
113+
rootCA, err := u.getRootCA(ctx, clusterAddress)
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
var (
119+
loraPFProfileID []byte
120+
loraPFProfile = &northboundv1.LoraPacketForwarderProfile{
121+
ProfileName: clusterAddress,
122+
Shared: false,
123+
Protocol: northboundv1.LoraPacketForwarderProtocol_LORA_PACKET_FORWARDER_PROTOCOL_BASIC_STATION,
124+
Address: clusterAddress,
125+
RootCa: rootCA.Raw,
126+
}
127+
loraPFProfileClient = northboundv1.NewLoraPacketForwarderProfileServiceClient(u.client)
128+
)
129+
loraPFGetRes, err := loraPFProfileClient.GetByName(
130+
ctx,
131+
&northboundv1.LoraPacketForwarderProfileServiceGetByNameRequest{
132+
Domain: u.client.Domain(ctx),
133+
Group: profileGroup,
134+
ProfileName: clusterAddress,
135+
},
136+
)
137+
if err != nil {
138+
if status.Code(err) != codes.NotFound {
139+
logger.WithError(err).Warn("Failed to get LoRa Packet Forwarder profile")
140+
return nil, err
141+
}
142+
res, err := loraPFProfileClient.Create(ctx, &northboundv1.LoraPacketForwarderProfileServiceCreateRequest{
143+
Domain: u.client.Domain(ctx),
144+
Group: profileGroup,
145+
LoraPacketForwarderProfile: loraPFProfile,
146+
})
147+
if err != nil {
148+
logger.WithError(err).Warn("Failed to create LoRa Packet Forwarder profile")
149+
return nil, err
150+
}
151+
loraPFProfileID = res.ProfileId
152+
} else {
153+
if profile := loraPFGetRes.LoraPacketForwarderProfile; profile.Shared != loraPFProfile.Shared ||
154+
profile.Protocol != loraPFProfile.Protocol ||
155+
!bytes.Equal(profile.RootCa, loraPFProfile.RootCa) {
156+
_, err := loraPFProfileClient.Update(ctx, &northboundv1.LoraPacketForwarderProfileServiceUpdateRequest{
157+
Domain: u.client.Domain(ctx),
158+
Group: profileGroup,
159+
ProfileId: loraPFGetRes.ProfileId,
160+
LoraPacketForwarderProfile: loraPFProfile,
161+
})
162+
if err != nil {
163+
logger.WithError(err).Warn("Failed to update LoRa Packet Forwarder profile")
164+
return nil, err
165+
}
166+
}
167+
loraPFProfileID = loraPFGetRes.ProfileId
168+
}
169+
170+
// Update the gateway with the Lora Packet Forwarder profile.
171+
_, err = gtwClient.Update(ctx, &northboundv1.GatewayServiceUpdateRequest{
172+
GatewayId: eui.MarshalNumber(),
173+
Domain: u.client.Domain(ctx),
174+
LoraPacketForwarderProfileId: &northboundv1.ProfileIDValue{
175+
Value: loraPFProfileID,
176+
},
177+
})
178+
if err != nil {
179+
logger.WithError(err).Warn("Failed to update gateway with profiles")
180+
return nil, err
181+
}
182+
183+
return &dcstypes.GatewayMetadata{
184+
LBSLNSKey: lnsKey,
185+
}, nil
186+
}
187+
188+
// createAPIKeys creates the CUPS and LNS API keys for the gateway.
189+
func (u *Upstream) createAPIKeys(
190+
ctx context.Context, ids *ttnpb.GatewayIdentifiers,
191+
) (cupsKey, lnsKey *ttnpb.APIKey, err error) {
192+
logger := log.FromContext(ctx)
193+
194+
gatewayAccess, err := u.getGatewayAccess(ctx)
195+
if err != nil {
196+
return nil, nil, err
197+
}
198+
199+
callOpt, err := rpcmetadata.WithForwardedAuth(ctx, u.AllowInsecureForCredentials())
200+
if err != nil {
201+
return nil, nil, err
202+
}
203+
204+
cupsKey, err = gatewayAccess.CreateAPIKey(ctx, &ttnpb.CreateGatewayAPIKeyRequest{
205+
GatewayIds: ids,
206+
Name: fmt.Sprintf("LBS CUPS Key (TTGC claim), generated %s", time.Now().UTC().Format(time.RFC3339)),
207+
Rights: []ttnpb.Right{
208+
ttnpb.Right_RIGHT_GATEWAY_INFO,
209+
ttnpb.Right_RIGHT_GATEWAY_SETTINGS_BASIC,
210+
ttnpb.Right_RIGHT_GATEWAY_READ_SECRETS,
211+
},
212+
}, callOpt)
213+
if err != nil {
214+
logger.WithError(err).Warn("Failed to create CUPS API key")
215+
return nil, nil, errCreateAPIKey.WithCause(err)
216+
}
217+
218+
lnsKey, err = gatewayAccess.CreateAPIKey(ctx, &ttnpb.CreateGatewayAPIKeyRequest{
219+
GatewayIds: ids,
220+
Name: fmt.Sprintf("LBS LNS Key (TTGC claim), generated %s", time.Now().UTC().Format(time.RFC3339)),
221+
Rights: []ttnpb.Right{
222+
ttnpb.Right_RIGHT_GATEWAY_LINK,
223+
},
224+
}, callOpt)
225+
if err != nil {
226+
logger.WithError(err).Warn("Failed to create LNS API key")
227+
return nil, nil, errCreateAPIKey.WithCause(err)
228+
}
229+
230+
return cupsKey, lnsKey, nil
231+
}
232+
233+
func (u *Upstream) getGatewayAccess(ctx context.Context) (ttnpb.GatewayAccessClient, error) {
234+
if u.gatewayAccess != nil {
235+
return u.gatewayAccess, nil
236+
}
237+
conn, err := u.GetPeerConn(ctx, ttnpb.ClusterRole_ACCESS, nil)
238+
if err != nil {
239+
return nil, err
240+
}
241+
return ttnpb.NewGatewayAccessClient(conn), nil
242+
}
243+
244+
// Unclaim implements gateways.Claimer.
245+
// Unclaim revokes the API keys and unclaims the gateway on TTGC.
246+
func (u *Upstream) Unclaim(ctx context.Context, eui types.EUI64) error {
247+
ids := &ttnpb.GatewayIdentifiers{
248+
Eui: eui.Bytes(),
249+
}
250+
251+
if err := u.deleteAPIKeys(ctx, ids); err != nil {
252+
return err
253+
}
254+
255+
// Unclaim the gateway on TTGC.
256+
gtwClient := northboundv1.NewGatewayServiceClient(u.client)
257+
_, err := gtwClient.Unclaim(ctx, &northboundv1.GatewayServiceUnclaimRequest{
258+
GatewayId: eui.MarshalNumber(),
259+
Domain: u.client.Domain(ctx),
260+
})
261+
if err != nil {
262+
if errors.IsNotFound(err) {
263+
// The gateway does not exist or is already unclaimed.
264+
return nil
265+
}
266+
return err
267+
}
268+
return nil
269+
}
270+
271+
var errDeleteAPIKey = errors.DefineAborted("delete_api_key", "delete API key")
272+
273+
// deleteAPIKeys deletes the CUPS and LNS API keys for the gateway.
274+
func (u *Upstream) deleteAPIKeys(ctx context.Context, ids *ttnpb.GatewayIdentifiers) error {
275+
logger := log.FromContext(ctx)
276+
277+
gatewayAccess, err := u.getGatewayAccess(ctx)
278+
if err != nil {
279+
return err
280+
}
281+
282+
callOpt, err := rpcmetadata.WithForwardedAuth(ctx, u.AllowInsecureForCredentials())
283+
if err != nil {
284+
return err
285+
}
286+
287+
apiKeys, err := gatewayAccess.ListAPIKeys(ctx, &ttnpb.ListGatewayAPIKeysRequest{
288+
GatewayIds: ids,
289+
}, callOpt)
290+
if err != nil {
291+
logger.WithError(err).Warn("Failed to list API keys")
292+
return errDeleteAPIKey.WithCause(err)
293+
}
294+
295+
// Delete the LBS CUPS and LBS LNS keys.
296+
for _, key := range apiKeys.ApiKeys {
297+
if key.Name == "" {
298+
continue
299+
}
300+
// Match keys created by this claimer.
301+
if len(key.Name) > 8 && (key.Name[:8] == "LBS CUPS" || key.Name[:7] == "LBS LNS") {
302+
_, err := gatewayAccess.DeleteAPIKey(ctx, &ttnpb.DeleteGatewayAPIKeyRequest{
303+
GatewayIds: ids,
304+
KeyId: key.Id,
305+
}, callOpt)
306+
if err != nil {
307+
logger.WithError(err).WithField("key_id", key.Id).Warn("Failed to delete API key")
308+
// Continue deleting other keys.
309+
}
310+
}
311+
}
312+
313+
return nil
314+
}
315+
316+
// IsManagedGateway implements gateways.Claimer.
317+
// This method always returns true.
318+
func (*Upstream) IsManagedGateway(context.Context, types.EUI64) (bool, error) {
319+
return true, nil
320+
}

0 commit comments

Comments
 (0)