forked from cloudnativelabs/kube-router
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.go
More file actions
620 lines (545 loc) · 19.5 KB
/
utils.go
File metadata and controls
620 lines (545 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
package proxy
import (
"context"
"errors"
"fmt"
"hash/fnv"
"net"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/cloudnativelabs/kube-router/v2/internal/nlretry"
"github.com/cloudnativelabs/kube-router/v2/pkg/cri"
"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/klog/v2"
netutils "k8s.io/utils/net"
)
const (
interfaceWaitSleepTime = 100 * time.Millisecond
sysFSVirtualNetPath = "/sys/devices/virtual/net"
sysFSHairpinRelPath = "brport/hairpin_mode"
hairpinEnable = "1"
)
func attemptNamespaceResetAfterError(hostNSHandle netns.NsHandle) {
err := netns.Set(hostNSHandle)
if err != nil {
klog.Errorf("failed to set hostNetworkNamespace while resetting namespace after a previous error due to %v",
err)
}
activeNetworkNamespaceHandle, err := netns.Get()
if err != nil {
klog.Errorf("failed to confirm activeNetworkNamespace while resetting namespace after "+
"a previous error due to %v", err)
return
}
klog.V(2).Infof("Current network namespace after revert namespace to host network namespace: %s",
activeNetworkNamespaceHandle.String())
_ = activeNetworkNamespaceHandle.Close()
}
// generateUniqueFWMark generates a unique uint32 hash value using the IP address, port, and protocol. This can then
// be used in IPVS, ip rules, and iptables to mark and later identify packets. FWMarks along with ip, port, and protocol
// are then stored in a map on the NSC and can be used later for lookup and as a general translation layer. If after
// maxUniqueFWMarkInc tries, generateUniqueFWMark is not able to find a unique permutation to use, an error is returned.
func (nsc *NetworkServicesController) generateUniqueFWMark(ip, protocol, port string) (uint32, error) {
// Generate a unit32 hash value using the IP address, port and protocol. This has been moved to an anonymous
// function since calling this without guarantees of uniqueness is unsafe.
generateFWMark := func(ip, protocol, port string, increment int) (uint32, error) {
const maxFwMarkBitSize = 0x3FFF
var err error
h := fnv.New32a()
if increment == 0 {
_, err = h.Write([]byte(ip + "-" + protocol + "-" + port))
} else {
_, err = h.Write([]byte(ip + "-" + protocol + "-" + port + "-" + fmt.Sprintf("%d", increment)))
}
if err != nil {
return 0, err
}
return h.Sum32() & maxFwMarkBitSize, err
}
const maxUniqueFWMarkInc = 16380
increment := 0
serviceKey := fmt.Sprintf("%s-%s-%s", ip, protocol, port)
for {
potentialFWMark, err := generateFWMark(ip, protocol, port, increment)
if err != nil {
return potentialFWMark, err
}
if foundServiceKey, ok := nsc.fwMarkMap[potentialFWMark]; ok {
if foundServiceKey != serviceKey {
increment++
continue
}
}
if increment >= maxUniqueFWMarkInc {
return 0, fmt.Errorf("could not obtain a unique FWMark for %s:%s:%s after %d tries",
protocol, ip, port, maxUniqueFWMarkInc)
}
nsc.fwMarkMap[potentialFWMark] = serviceKey
return potentialFWMark, nil
}
}
// lookupFWMarkByService finds the related FW mark from the internal fwMarkMap kept by the NetworkServiceController
// given the related ip, protocol, and port. If it isn't able to find a matching FW mark, then it returns an error.
func (nsc *NetworkServicesController) lookupFWMarkByService(ip, protocol, port string) uint32 {
needle := fmt.Sprintf("%s-%s-%s", ip, protocol, port)
for fwMark, serviceKey := range nsc.fwMarkMap {
if needle == serviceKey {
return fwMark
}
}
return 0
}
// lookupServiceByFWMark Lookup service ip, protocol, port by given FW Mark value (reverse of lookupFWMarkByService)
func (nsc *NetworkServicesController) lookupServiceByFWMark(fwMark uint32) (string, string, int, error) {
serviceKey, ok := nsc.fwMarkMap[fwMark]
if !ok {
return "", "", 0, fmt.Errorf("could not find service matching the given FW mark")
}
serviceKeySplit := strings.Split(serviceKey, "-")
if len(serviceKeySplit) != 3 {
return "", "", 0, fmt.Errorf("service key for found FW mark did not have 3 parts, this shouldn't be possible")
}
port, err := strconv.ParseInt(serviceKeySplit[2], 10, 32)
if err != nil {
return "", "", 0, fmt.Errorf("port number for service key for found FW mark was not a 32-bit int: %v", err)
}
return serviceKeySplit[0], serviceKeySplit[1], int(port), nil
}
// isValidKubeRouterServiceArtifact looks up a service by its clusterIP, externalIP, or loadBalancerIP. It returns
// truthy
func (nsc *NetworkServicesController) isValidKubeRouterServiceArtifact(address net.IP, nodePort int) (bool, error) {
for _, svc := range nsc.getServiceMap() {
for _, clIP := range svc.clusterIPs {
if net.ParseIP(clIP).Equal(address) {
return true, nil
}
}
for _, exIP := range svc.externalIPs {
if net.ParseIP(exIP).Equal(address) {
return true, nil
}
}
for _, lbIP := range svc.loadBalancerIPs {
if net.ParseIP(lbIP).Equal(address) {
return true, nil
}
}
if nodePort != 0 && svc.nodePort == nodePort {
if nsc.nodeportBindOnAllIP {
addrMap, err := getAllLocalIPs()
if err != nil {
return false, fmt.Errorf("failed to get all local IPs: %v", err)
}
var addresses []net.IP
if address.To4() != nil {
addresses = addrMap[v1.IPv4Protocol]
} else {
addresses = addrMap[v1.IPv6Protocol]
}
for _, addr := range addresses {
if addr.Equal(address) {
return true, nil
}
}
} else if address.Equal(nsc.krNode.GetPrimaryNodeIP()) {
return true, nil
}
}
}
return false, fmt.Errorf("service not found for address %s", address.String())
}
// unsortedListsEquivalent compares two lists of endpointsInfo and considers them the same if they contains the same
// contents regardless of order. Returns true if both lists contain the same contents.
func unsortedListsEquivalent(a, b []endpointSliceInfo) bool {
if len(a) != len(b) {
return false
}
values := make(map[interface{}]int)
for _, val := range a {
values[val] = 1
}
for _, val := range b {
values[val]++
}
for _, val := range values {
if val == 1 {
return false
}
}
return true
}
// endpointsMapsEquivalent compares two maps of endpointsInfoMap to see if they have the same keys and values. Returns
// true if both maps contain the same keys and values.
func endpointsMapsEquivalent(a, b endpointSliceInfoMap) bool {
if len(a) != len(b) {
return false
}
for key, valA := range a {
valB, ok := b[key]
if !ok {
return false
}
if !unsortedListsEquivalent(valA, valB) {
return false
}
}
return true
}
// convertSvcProtoToSysCallProto converts a string based protocol that we receive from Kubernetes via something like the
// serviceInfo object into the uint16 syscall version of the protocol that is capable of interfacing with aspects of the
// Linux sub-sysem like IPVS
func convertSvcProtoToSysCallProto(svcProtocol string) uint16 {
switch svcProtocol {
case tcpProtocol:
return syscall.IPPROTO_TCP
case udpProtocol:
return syscall.IPPROTO_UDP
case sctpProtocol:
return syscall.IPPROTO_SCTP
default:
return syscall.IPPROTO_NONE
}
}
// convertSysCallProtoToSvcProto converts a syscall based protocol version to a string representation that Kubernetes
// and other parts of kube-router understand
func convertSysCallProtoToSvcProto(sysProtocol uint16) string {
switch sysProtocol {
case syscall.IPPROTO_TCP:
return tcpProtocol
case syscall.IPPROTO_UDP:
return udpProtocol
case syscall.IPPROTO_SCTP:
return sctpProtocol
default:
return noneProtocol
}
}
// findContainerRuntimeReferences find the container runtime and container ID for a given endpoint IP do this by:
// - Resolving the endpoint IP to a pod
// - Ensure that the pod actually exists on the node in question
// - Get the container ID of the primary container (since this function primarily allows us to enter the pod's
// namespace, it doesn't really matter which container we choose here, if this function gets used for something
// else in the future, this might have to be re-evaluated)
func (nsc *NetworkServicesController) findContainerRuntimeReferences(endpointIP string) (string, string, error) {
podObj, err := nsc.getPodObjectForEndpointIP(endpointIP)
if err != nil {
return "", "", fmt.Errorf("failed to find endpoint with ip: %s. so skipping preparing endpoint for DSR",
endpointIP)
}
// we are only concerned with endpoint pod running on current node
if strings.Compare(podObj.Status.HostIP, nsc.krNode.GetPrimaryNodeIP().String()) != 0 {
return "", "", nil
}
containerURL := podObj.Status.ContainerStatuses[0].ContainerID
runtime, containerID, err := cri.EndpointParser(containerURL)
if err != nil {
return "", "", fmt.Errorf("couldn't get containerID (container=%s, pod=%s). Skipping DSR endpoint set up",
podObj.Spec.Containers[0].Name, podObj.Name)
}
if containerID == "" {
return "", "", fmt.Errorf("failed to find container id for the endpoint with ip: %s so skipping preparing "+
"endpoint for DSR", endpointIP)
}
return runtime, containerID, nil
}
// addDSRIPInsidePodNetNamespace takes a given external IP and endpoint IP for a DSR service and then uses the container
// runtime to add the external IP to a virtual interface inside the pod so that it can receive DSR traffic inside its
// network namespace.
func (nsc *NetworkServicesController) addDSRIPInsidePodNetNamespace(externalIP, endpointIP string) error {
crRuntime, containerID, err := nsc.findContainerRuntimeReferences(endpointIP)
if err != nil {
return err
}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
hostNetworkNamespaceHandle, err := netns.Get()
if err != nil {
return fmt.Errorf("failed to get namespace due to %v", err)
}
defer utils.CloseCloserDisregardError(&hostNetworkNamespaceHandle)
var pid int
if crRuntime == "docker" {
// WARN: This method is deprecated and will be removed once docker-shim is removed from kubelet.
pid, err = nsc.ln.getContainerPidWithDocker(containerID)
if err != nil {
return fmt.Errorf("failed to prepare endpoint %s to do direct server return due to %v",
endpointIP, err)
}
} else {
// We expect CRI compliant runtimes here
// ugly workaround, refactoring of pkg/Proxy is required
pid, err = nsc.ln.getContainerPidWithCRI(nsc.dsr.runtimeEndpoint, containerID)
if err != nil {
return fmt.Errorf("failed to prepare endpoint %s to do DSR due to: %v", endpointIP, err)
}
}
return nsc.ln.configureContainerForDSR(externalIP, endpointIP, containerID, pid, hostNetworkNamespaceHandle)
}
// getPrimaryAndCIDRsByFamily returns the best primary nodeIP and a slice of all of the relevant podCIDRs based upon a
// given IP family
func (nsc *NetworkServicesController) getPrimaryAndCIDRsByFamily(ipFamily v1.IPFamily) (string, []string) {
var primaryIP string
cidrMap := make(map[string]bool)
//nolint:exhaustive // we don't need exhaustive searching for IP Families
switch ipFamily {
case v1.IPv4Protocol:
// If we're not detected to be IPv4 capable break early
if !nsc.krNode.IsIPv4Capable() {
return "", nil
}
primaryIP = nsc.krNode.FindBestIPv4NodeAddress().String()
if len(nsc.podCidr) > 0 && netutils.IsIPv4CIDRString(nsc.podCidr) {
cidrMap[nsc.podCidr] = true
}
if len(nsc.podIPv4CIDRs) > 0 {
for _, cidr := range nsc.podIPv4CIDRs {
if _, ok := cidrMap[cidr]; !ok {
cidrMap[cidr] = true
}
}
}
case v1.IPv6Protocol:
// If we're not detected to be IPv6 capable break early
if !nsc.krNode.IsIPv6Capable() {
return "", nil
}
primaryIP = nsc.krNode.FindBestIPv6NodeAddress().String()
if len(nsc.podCidr) > 0 && netutils.IsIPv6CIDRString(nsc.podCidr) {
cidrMap[nsc.podCidr] = true
}
if len(nsc.podIPv6CIDRs) > 0 {
for _, cidr := range nsc.podIPv6CIDRs {
if _, ok := cidrMap[cidr]; !ok {
cidrMap[cidr] = true
}
}
}
}
cidrs := make([]string, len(cidrMap))
idx := 0
for cidr := range cidrMap {
cidrs[idx] = cidr
idx++
}
return primaryIP, cidrs
}
func (nsc *NetworkServicesController) getPodObjectForEndpointIP(endpointIP string) (*v1.Pod, error) {
for _, obj := range nsc.podLister.List() {
pod := obj.(*v1.Pod)
for _, ip := range pod.Status.PodIPs {
if strings.Compare(ip.IP, endpointIP) == 0 {
return pod, nil
}
}
}
return nil, errors.New("Failed to find pod with ip " + endpointIP)
}
func (nsc *NetworkServicesController) getServiceForServiceInfo(svcIn *serviceInfo) (*v1.Service, error) {
svc, err := nsc.client.CoreV1().Services(svcIn.namespace).Get(context.Background(), svcIn.name, metav1.GetOptions{})
if err != nil {
return nil, err
}
return svc, err
}
func (nsc *NetworkServicesController) getPodListForService(svc *v1.Service) (*v1.PodList, error) {
lbSet := labels.Set(svc.Spec.Selector)
listOpts := metav1.ListOptions{LabelSelector: lbSet.AsSelector().String()}
pods, err := nsc.client.CoreV1().Pods(svc.Namespace).List(context.Background(), listOpts)
if err != nil {
return nil, err
}
return pods, err
}
// GetAllClusterIPs returns all of the cluster IPs on a service separated into IPv4 and IPv6 lists
func getAllClusterIPs(svc *serviceInfo) map[v1.IPFamily][]net.IP {
// We use maps here so that we can de-duplicate repeat IP addresses
v4Map := make(map[string]bool)
v6Map := make(map[string]bool)
if svc.clusterIP != nil {
if svc.clusterIP.To4() != nil {
v4Map[svc.clusterIP.String()] = true
} else {
v6Map[svc.clusterIP.String()] = true
}
}
for _, clIP := range svc.clusterIPs {
ip := net.ParseIP(clIP)
if ip == nil {
continue
}
if ip.To4() != nil {
v4Map[ip.String()] = true
} else {
v6Map[ip.String()] = true
}
}
return convertIPMapsToFamilyMap(v4Map, v6Map)
}
// getAllClusterIPs returns all of the cluster IPs on a service separated into IPv4 and IPv6 lists
func getAllExternalIPs(svc *serviceInfo, includeLBIPs bool) map[v1.IPFamily][]net.IP {
// We use maps here so that we can de-duplicate repeat IP addresses
v4Map := make(map[string]bool)
v6Map := make(map[string]bool)
for _, exIP := range svc.externalIPs {
ip := net.ParseIP(exIP)
if ip == nil {
continue
}
if ip.To4() != nil {
v4Map[ip.String()] = true
} else {
v6Map[ip.String()] = true
}
}
if !includeLBIPs {
return convertIPMapsToFamilyMap(v4Map, v6Map)
}
for _, lbIP := range svc.loadBalancerIPs {
ip := net.ParseIP(lbIP)
if ip == nil {
continue
}
if ip.To4() != nil {
v4Map[ip.String()] = true
} else {
v6Map[ip.String()] = true
}
}
return convertIPMapsToFamilyMap(v4Map, v6Map)
}
// convertIPMapsToFamilyMap converts family specific maps of string IPs to a single map that is keyed by IPFamily and
// has parsed net.IPs
func convertIPMapsToFamilyMap(v4Map map[string]bool, v6Map map[string]bool) map[v1.IPFamily][]net.IP {
allIPs := make(map[v1.IPFamily][]net.IP)
allIPs[v1.IPv4Protocol] = make([]net.IP, len(v4Map))
allIPs[v1.IPv6Protocol] = make([]net.IP, len(v6Map))
idx := 0
for ip := range v4Map {
allIPs[v1.IPv4Protocol][idx] = net.ParseIP(ip)
idx++
}
idx = 0
for ip := range v6Map {
allIPs[v1.IPv6Protocol][idx] = net.ParseIP(ip)
idx++
}
return allIPs
}
// hairpinRuleFrom create hairpin rules for a given set of parameters
func hairpinRuleFrom(serviceIPs []net.IP, endpointIP string, endpointFamily v1.IPFamily, servicePort int,
ruleMap map[string][]string) {
var vipSubnet string
//nolint:exhaustive // we don't need exhaustive searching for IP Families
switch endpointFamily {
case v1.IPv4Protocol:
vipSubnet = "/32"
case v1.IPv6Protocol:
vipSubnet = "/128"
}
for _, svcIP := range serviceIPs {
ruleArgs := []string{"-s", endpointIP + vipSubnet, "-d", endpointIP + vipSubnet,
"-m", "ipvs", "--vaddr", svcIP.String(), "--vport", strconv.Itoa(servicePort),
"-j", "SNAT", "--to-source", svcIP.String()}
// Trying to ensure this matches iptables.List()
ruleString := "-A " + ipvsHairpinChainName + " -s " + endpointIP + vipSubnet + " -d " +
endpointIP + vipSubnet + " -m ipvs" + " --vaddr " + svcIP.String() + " --vport " +
strconv.Itoa(servicePort) + " -j SNAT" + " --to-source " + svcIP.String()
ruleMap[ruleString] = ruleArgs
}
}
// ensureHairpinChain make sure that the hairpin chain in the nat table exists and that it has a jump rule from the
// POSTROUTING chain
func ensureHairpinChain(iptablesCmdHandler utils.IPTablesHandler) error {
hasHairpinChain := false
chains, err := iptablesCmdHandler.ListChains("nat")
if err != nil {
return fmt.Errorf("failed to list iptables chains: %v", err)
}
for _, chain := range chains {
if chain == ipvsHairpinChainName {
hasHairpinChain = true
}
}
// Create a chain for hairpin rules, if needed
if !hasHairpinChain {
err = iptablesCmdHandler.NewChain("nat", ipvsHairpinChainName)
if err != nil {
return fmt.Errorf("failed to create iptables chain \"%s\": %v", ipvsHairpinChainName, err)
}
}
// Create a jump that points to our hairpin chain
jumpArgs := []string{"-m", "ipvs", "--vdir", "ORIGINAL", "-j", ipvsHairpinChainName}
err = iptablesCmdHandler.AppendUnique("nat", "POSTROUTING", jumpArgs...)
if err != nil {
return fmt.Errorf("failed to add hairpin iptables jump rule: %v", err)
}
return nil
}
// getAllLocalIPs returns all IP addresses found on any network address in the system, excluding dummy and docker
// interfaces in a map that distinguishes between IPv4 and IPv6 addresses by v1.IPFamily
func getAllLocalIPs() (map[v1.IPFamily][]net.IP, error) {
// We use maps here so that we can de-duplicate repeat IP addresses
v4Map := make(map[string]bool)
v6Map := make(map[string]bool)
links, err := nlretry.LinkList(context.Background())
if err != nil {
return nil, fmt.Errorf("could not load list of net interfaces: %v", err)
}
for _, link := range links {
// do not include IPs for any interface that calls itself "dummy", "kube", or "docker"
if strings.Contains(link.Attrs().Name, "dummy") ||
strings.Contains(link.Attrs().Name, "kube") ||
strings.Contains(link.Attrs().Name, "docker") {
continue
}
linkAddrs, err := nlretry.AddrList(context.Background(), link, netlink.FAMILY_ALL)
if err != nil {
return nil, fmt.Errorf("failed to get IPs for interface: %v", err)
}
for _, addr := range linkAddrs {
if addr.IP.To4() != nil {
v4Map[addr.IP.String()] = true
} else {
v6Map[addr.IP.String()] = true
}
}
}
return convertIPMapsToFamilyMap(v4Map, v6Map), nil
}
// getIPSetName formulates an IP Family specific ipset name based upon the prefix and IPFamily passed
func getIPSetName(nameBase string, family v1.IPFamily) string {
var sb strings.Builder
if family == v1.IPv6Protocol {
sb.WriteString("inet6:")
}
sb.WriteString(nameBase)
return sb.String()
}
// getIPVSFirewallInputChainRule creates IPVS firwall input chain rule based upon the family that is passed. This is
// used by the NSC to ensure that traffic destined for IPVS services on the INPUT table will be directed to the IPVS
// firewall chain
func getIPVSFirewallInputChainRule(family v1.IPFamily) []string {
// The iptables rule for use in {setup,cleanup}IpvsFirewall.
return []string{
"-m", "comment", "--comment", "handle traffic to IPVS service IPs in custom chain",
"-m", "set", "--match-set", getIPSetName(serviceIPsIPSetName, family), "dst",
"-j", ipvsFirewallChainName}
}
// getLabelFromMap checks the list of passed labels for the service.kubernetes.io/service-proxy-name
// label and if it exists, returns it otherwise returns an error
func getLabelFromMap(label string, labels map[string]string) (string, error) {
for lbl, val := range labels {
if lbl == label {
return val, nil
}
}
return "", fmt.Errorf("label doesn't exist in map")
}