• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hknutzen / Netspoc-Approve / 16674559403

01 Aug 2025 12:03PM UTC coverage: 98.531% (-0.07%) from 98.6%
16674559403

push

github

hknutzen
CHANGELOG file

5230 of 5308 relevant lines covered (98.53%)

1.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.75
/go/pkg/cisco/diff.go
1
package cisco
2

3
import (
4
        "cmp"
5
        "fmt"
6
        "maps"
7
        "net"
8
        "net/netip"
9
        "regexp"
10
        "slices"
11
        "sort"
12
        "strconv"
13
        "strings"
14

15
        "github.com/hknutzen/Netspoc-Approve/go/pkg/console"
16
        "github.com/hknutzen/Netspoc-Approve/go/pkg/errlog"
17
        "github.com/pkg/diff/edit"
18
        "github.com/pkg/diff/myers"
19
)
20

21
type State struct {
22
        *parser
23
        Conn         *console.Conn
24
        errUnmanaged []error
25
        DeviceCfg    *Config
26
        SpocCfg      *Config
27
        Changes      []string
28
        subCmdOf     string
29
}
30

31
func (s *State) addChange(chg string) {
1✔
32
        s.Changes = append(s.Changes, chg)
1✔
33
}
1✔
34

35
func (s *State) GetChanges() error {
1✔
36
        s.alignVRFs()
1✔
37
        if err := s.checkInterfaces(); err != nil {
2✔
38
                return err
1✔
39
        }
1✔
40
        s.ignoreCryptoGDOI()
1✔
41
        s.diffConfig()
1✔
42
        return nil
1✔
43
}
44

45
func (s *State) HasChanges() bool {
1✔
46
        return len(s.Changes) != 0
1✔
47
}
1✔
48

49
func (s *State) ShowChanges() string {
1✔
50
        var collect strings.Builder
1✔
51
        for _, chg := range s.Changes {
2✔
52
                chg = strings.Replace(chg, "\n", "\\N ", 1)
1✔
53
                fmt.Fprintln(&collect, chg)
1✔
54
        }
1✔
55
        return collect.String()
1✔
56
}
57

58
func (s *State) diffConfig() {
1✔
59
        s.addDefaults(s.DeviceCfg)
1✔
60
        s.addDefaults(s.SpocCfg)
1✔
61
        sortGroups(s.DeviceCfg)
1✔
62
        sortGroups(s.SpocCfg)
1✔
63
        sortRoutes(s.DeviceCfg)
1✔
64
        sortRoutes(s.SpocCfg)
1✔
65
        s.generateNamesForTransfer()
1✔
66
        comb := make(objLookup)
1✔
67
        maps.Copy(comb, s.DeviceCfg.lookup)
1✔
68
        maps.Copy(comb, s.SpocCfg.lookup)
1✔
69
        for _, prefix := range slices.Sorted(maps.Keys(comb)) {
2✔
70
                if prefix == "tunnel-group-map" {
2✔
71
                        s.diffTunnelGroupMap()
1✔
72
                } else if prefix == "webvpn" {
3✔
73
                        s.diffWebVPN()
1✔
74
                } else {
2✔
75
                        var anchor bool
1✔
76
                        for _, l := range comb[prefix] {
2✔
77
                                anchor = l[0].typ.anchor
1✔
78
                                break
1✔
79
                        }
80
                        if anchor {
2✔
81
                                s.diffAnchors(prefix)
1✔
82
                        } else {
2✔
83
                                s.diffSomeAnchors(prefix)
1✔
84
                        }
1✔
85
                }
86
        }
87
        s.deleteUnused()
1✔
88
}
89

90
func byParsedCmd(_ *Config, c *cmd) string {
1✔
91
        return c.parsed
1✔
92
}
1✔
93

94
func (s *State) diffAnchors(prefix string) {
1✔
95
        aMap := s.DeviceCfg.lookup[prefix]
1✔
96
        bMap := s.SpocCfg.lookup[prefix]
1✔
97
        aNames := slices.Sorted(maps.Keys(aMap))
1✔
98
        bNames := slices.Sorted(maps.Keys(bMap))
1✔
99
        s.diffNamedCmds2(aMap, bMap, aNames, bNames)
1✔
100
}
1✔
101

102
func (s *State) diffSomeAnchors(prefix string) {
1✔
103
        aMap := s.DeviceCfg.lookup[prefix]
1✔
104
        bMap := s.SpocCfg.lookup[prefix]
1✔
105
        onlyAnchorNames := func(m map[string][]*cmd) []string {
2✔
106
                var result []string
1✔
107
                for name, l := range m {
2✔
108
                        if l[0].anchor {
2✔
109
                                result = append(result, name)
1✔
110
                        }
1✔
111
                }
112
                sort.Strings(result)
1✔
113
                return result
1✔
114
        }
115
        aNames := onlyAnchorNames(aMap)
1✔
116
        bNames := onlyAnchorNames(bMap)
1✔
117
        s.diffNamedCmds2(aMap, bMap, aNames, bNames)
1✔
118
}
119

120
func (s *State) diffNamedCmds2(
121
        aMap, bMap map[string][]*cmd, aNames, bNames []string) {
1✔
122

1✔
123
        for _, aName := range aNames {
2✔
124
                s.diffCmds(aMap[aName], bMap[aName], byParsedCmd)
1✔
125
        }
1✔
126
        for _, bName := range bNames {
2✔
127
                if _, found := aMap[bName]; !found {
2✔
128
                        s.diffCmds(nil, bMap[bName], byParsedCmd)
1✔
129
                }
1✔
130
        }
131
}
132

133
// Use "subject-name" of referenced "crypto ca certificate map" as key.
134
func byCertMapKey(cf *Config, c *cmd) string {
1✔
135
        if len(c.ref) == 1 {
2✔
136
                // tunnel-group-map default-group $tunnel-group
1✔
137
                return "default-group"
1✔
138
        }
1✔
139
        name := c.ref[0]
1✔
140
        prefix := c.typ.ref[0]
1✔
141
        l := cf.lookup[prefix][name]
1✔
142
        for _, sc := range l[0].sub {
2✔
143
                if strings.HasPrefix(sc.parsed, "subject-name attr") {
2✔
144
                        return sc.parsed
1✔
145
                }
1✔
146
        }
147
        return ""
1✔
148
}
149

150
func (s *State) diffTunnelGroupMap() {
1✔
151
        prefix := "tunnel-group-map"
1✔
152
        al := s.DeviceCfg.lookup[prefix][""]
1✔
153
        bl := s.SpocCfg.lookup[prefix][""]
1✔
154
        s.diffCmds(al, bl, byCertMapKey)
1✔
155
}
1✔
156

157
func (s *State) diffWebVPN() {
1✔
158
        prefix := "webvpn"
1✔
159
        al := s.DeviceCfg.lookup[prefix][""]
1✔
160
        bl := s.SpocCfg.lookup[prefix][""]
1✔
161
        if al == nil {
2✔
162
                if bl != nil {
2✔
163
                        s.addCmds(bl)
1✔
164
                }
1✔
165
        } else if bl != nil {
2✔
166
                for _, aCmd := range al {
2✔
167
                        aCmd.needed = true
1✔
168
                }
1✔
169
                s.diffCmds(al[0].sub, bl[0].sub, byCertMapKey)
1✔
170
        } else {
1✔
171
                s.delCmds(al[0].sub)
1✔
172
        }
1✔
173
}
174

175
type keyFunc func(*Config, *cmd) string
176

177
type cmdsPair struct {
178
        a, b         *Config
179
        aCmds, bCmds []*cmd
180
        key          keyFunc
181
}
182

183
func (ab *cmdsPair) LenA() int { return len(ab.aCmds) }
1✔
184
func (ab *cmdsPair) LenB() int { return len(ab.bCmds) }
1✔
185

186
func (ab *cmdsPair) Equal(ai, bi int) bool {
1✔
187
        return ab.key(ab.a, ab.aCmds[ai]) == ab.key(ab.b, ab.bCmds[bi])
1✔
188
}
1✔
189

190
// Compare list of commands
191
// - toplevel commands having equal prefix and to be compared names or
192
// - subcommands of otherwise equal toplevel command.
193
// Return name of existing command, if it is unchanged or modified
194
// or return name of new command, if it replaces old command.
195
func (s *State) diffCmds(al, bl []*cmd, key keyFunc) string {
1✔
196
        // Command on device was already equalized with other command from Netspoc.
1✔
197
        if len(al) > 0 && al[0].needed {
2✔
198
                if len(bl) > 0 {
2✔
199
                        s.addCmds(bl)
1✔
200
                        return bl[0].name
1✔
201
                }
1✔
202
                return ""
×
203
        }
204
        if len(bl) > 0 {
2✔
205
                c := bl[0]
1✔
206
                // Command from Netspoc already was transferred or was found on device.
1✔
207
                if c.ready {
2✔
208
                        return c.name
1✔
209
                }
1✔
210
                // Find equal simple object on device or transfer.
211
                if c.typ.simpleObj {
2✔
212
                        return s.equalizeSimpleObject(al, bl)
1✔
213
                }
1✔
214
        }
215
        ab := &cmdsPair{
1✔
216
                a:     s.DeviceCfg,
1✔
217
                b:     s.SpocCfg,
1✔
218
                aCmds: al,
1✔
219
                bCmds: bl,
1✔
220
                key:   key,
1✔
221
        }
1✔
222
        hasEq := false
1✔
223
        isEq := true
1✔
224
        diff := diffCmdLists(ab)
1✔
225
        for _, r := range diff {
2✔
226
                if r.IsEqual() {
2✔
227
                        hasEq = true
1✔
228
                } else {
2✔
229
                        isEq = false
1✔
230
                }
1✔
231
        }
232
        if len(al) > 0 {
2✔
233
                if p := al[0].typ.prefix; p == "route" ||
1✔
234
                        p == "ip route" || p == "ipv6 route" {
2✔
235

1✔
236
                        s.diffRoutes(al, bl, diff)
1✔
237
                        return ""
1✔
238
                }
1✔
239
        }
240
        // Standard ACL can't be changed incrementally.
241
        // Ignore "access-list $NAME remark " in first line.
242
        if !isEq {
2✔
243
                for _, c := range al {
2✔
244
                        if strings.HasPrefix(c.parsed, "access-list $NAME extended ") {
2✔
245
                                break
1✔
246
                        }
247
                        if strings.HasPrefix(c.parsed, "access-list $NAME standard ") {
2✔
248
                                hasEq = false
1✔
249
                                break
1✔
250
                        }
251
                }
252
        }
253
        // No parts are equal, hence remove all from device and add all from Netspoc.
254
        if !hasEq {
2✔
255
                if len(al) > 0 {
2✔
256
                        // Must not delete complete toplevel command, because at this time
1✔
257
                        // it is not known, if it is referenced by some other command.
1✔
258
                        if al[0].subCmdOf == nil {
2✔
259
                                s.markDeleted(al)
1✔
260
                        } else {
2✔
261
                                s.delCmds(al)
1✔
262
                        }
1✔
263
                }
264
                if len(bl) > 0 {
2✔
265
                        s.addCmds(bl)
1✔
266
                        return bl[0].name
1✔
267
                }
1✔
268
                return ""
1✔
269
        }
270
        // Commands modify existing command on device.
271
        // Use name and seq num of existing command in added commands.
272
        // But must not modify seq num of unnamed command "tunnel-group-map".
273
        if name := al[0].name; name != "" {
2✔
274
                seq := al[0].seq
1✔
275
                for _, r := range diff {
2✔
276
                        if r.IsInsert() {
2✔
277
                                for _, bCmd := range bl[r.LowB:r.HighB] {
2✔
278
                                        bCmd.name = name
1✔
279
                                        bCmd.seq = seq
1✔
280
                                }
1✔
281
                        }
282
                }
283
        }
284

285
        if al[0].typ.prefix == "access-list" {
2✔
286
                s.diffASAACLs(al, bl, diff)
1✔
287
                return al[0].name
1✔
288
        }
1✔
289
        if c := al[0].subCmdOf; c != nil &&
1✔
290
                c.typ.prefix == "ip access-list extended" {
2✔
291

1✔
292
                s.diffIOSACLs(al, bl, diff)
1✔
293
                return al[0].name
1✔
294
        }
1✔
295
        // Delete commands before adding new ones
296
        for _, r := range diff {
2✔
297
                if r.IsDelete() {
2✔
298
                        s.delCmds(al[r.LowA:r.HighA])
1✔
299
                }
1✔
300
        }
301
        for _, r := range diff {
2✔
302
                if r.IsInsert() {
2✔
303
                        s.addCmds(bl[r.LowB:r.HighB])
1✔
304
                } else if r.IsEqual() {
3✔
305
                        s.makeEqual(al[r.LowA:r.HighA], bl[r.LowB:r.HighB])
1✔
306
                }
1✔
307
        }
308
        return al[0].name
1✔
309
}
310

311
// al and bl are lists of commands, known to be equal, but names may differ.
312
// Equalize subcommands and referenced commands.
313
// Overwrite command
314
func (s *State) makeEqual(al, bl []*cmd) {
1✔
315
        for i, a := range al {
2✔
316
                b := bl[i]
1✔
317
                a.needed = true
1✔
318
                b.name = a.name
1✔
319
                b.seq = a.seq
1✔
320
                b.ready = true
1✔
321
                s.diffCmds(a.sub, b.sub, byParsedCmd)
1✔
322
                changedRef := false
1✔
323
                for i, aName := range a.ref {
2✔
324
                        prefix := a.typ.ref[i]
1✔
325
                        bName := b.ref[i]
1✔
326
                        aRef := s.DeviceCfg.lookup[prefix][aName]
1✔
327
                        bRef := s.SpocCfg.lookup[prefix][bName]
1✔
328
                        var refName string
1✔
329
                        if prefix == "aaa-server" && aName != bName {
2✔
330
                                refName = bName
1✔
331
                                s.addCmds(bRef)
1✔
332
                        } else if prefix == "crypto map" {
3✔
333
                                refName = s.diffCryptoMap(aRef, bRef)
1✔
334
                        } else {
2✔
335
                                refName = s.diffCmds(aRef, bRef, byParsedCmd)
1✔
336
                        }
1✔
337
                        if refName != aName {
2✔
338
                                changedRef = true
1✔
339
                        }
1✔
340
                }
341
                if changedRef {
2✔
342
                        if strings.Contains(b.parsed, "$NAME $SEQ set ikev") {
2✔
343
                                s.addChange("no " + a.orig)
1✔
344
                        }
1✔
345
                        s.addCmd(b)
1✔
346
                }
347
        }
348
}
349

350
func (s *State) equalizeSimpleObject(al, bl []*cmd) string {
1✔
351
        if !simpleObjEqual(al, bl) {
2✔
352
                s.markDeleted(al)
1✔
353
                al = s.findSimpleObjOnDevice(bl)
1✔
354
        }
1✔
355
        if al != nil {
2✔
356
                al[0].needed = true
1✔
357
                bl[0].ready = true
1✔
358
                bl[0].name = al[0].name
1✔
359
                return al[0].name
1✔
360
        }
1✔
361
        s.addCmds(bl)
1✔
362
        return bl[0].name
1✔
363
}
364

365
func (s *State) findSimpleObjOnDevice(bl []*cmd) []*cmd {
1✔
366
        return findSimpleObject(bl, s.DeviceCfg)
1✔
367
}
1✔
368

369
func findSimpleObject(bl []*cmd, a *Config) []*cmd {
1✔
370
        prefix := bl[0].typ.prefix
1✔
371
        m := a.lookup[prefix]
1✔
372
        for _, name := range slices.Sorted(maps.Keys(m)) {
2✔
373
                al := m[name]
1✔
374
                if simpleObjEqual(al, bl) {
2✔
375
                        return al
1✔
376
                }
1✔
377
        }
378
        return nil
1✔
379
}
380

381
func simpleObjEqual(al, bl []*cmd) bool {
1✔
382
        // Simple objects are known to have exactly one toplevel command.
1✔
383
        ac, bc := al[0], bl[0]
1✔
384
        sortedSub := func(c *cmd) []string {
2✔
385
                l := make([]string, len(c.sub))
1✔
386
                for i, s := range c.sub {
2✔
387
                        l[i] = s.parsed
1✔
388
                }
1✔
389
                sort.Strings(l)
1✔
390
                return l
1✔
391
        }
392
        return ac.parsed == bc.parsed && slices.Equal(sortedSub(ac), sortedSub(bc))
1✔
393
}
394

395
func (s *State) diffIOSACLs(al, bl []*cmd, diff []edit.Range) {
1✔
396
        aclName := al[0].subCmdOf.name
1✔
397
        s.addToplevel("ip access-list resequence " + aclName + " 10000 10000")
1✔
398
        chgLen := len(s.Changes)
1✔
399
        idx2Block, maxID := markIOSPermitDenyBlocks(al)
1✔
400
        type cmdAndPos struct {
1✔
401
                cmd *cmd
1✔
402
                pos int
1✔
403
        }
1✔
404
        // Collect to be deleted entries.
1✔
405
        var del []*cmdAndPos
1✔
406
        // Lookup map to check if added ACL line is already present on device.
1✔
407
        delMap := make(map[string]*cmdAndPos)
1✔
408
        stripLogRX := regexp.MustCompile(` log(?:-input)?`)
1✔
409
        addACL := func(b *cmd, before, i int) {
2✔
410
                lineNr := before*10000 + i + 1
1✔
411
                b.parsed = strconv.Itoa(lineNr) + " " + b.parsed
1✔
412
                s.addCmds([]*cmd{b})
1✔
413
        }
1✔
414
        delACL := func(a *cmdAndPos) {
2✔
415
                lineNr := (a.pos + 1) * 10000
1✔
416
                a.cmd.orig = strconv.Itoa(lineNr)
1✔
417
                s.delCmds([]*cmd{a.cmd})
1✔
418
        }
1✔
419
        // Generate move command which sends add and delete command together
420
        // as a single command.
421
        // Ignore move if both positions belong to the same block.
422
        moveACL := func(a *cmdAndPos, b *cmd, before, i int, moveOK bool) {
2✔
423
                defer func() { a.cmd = nil }()
2✔
424
                if moveOK {
2✔
425
                        oldID := idx2Block[a.pos]
1✔
426
                        if before > 0 && idx2Block[before-1] == oldID {
2✔
427
                                return
1✔
428
                        }
1✔
429
                        if before < len(idx2Block) && idx2Block[before] == oldID {
2✔
430
                                return
1✔
431
                        }
1✔
432
                }
433
                delACL(a)
1✔
434
                delIdx := len(s.Changes) - 1
1✔
435
                addACL(b, before, i)
1✔
436
                top := len(s.Changes) - 1
1✔
437
                del := s.Changes[delIdx]
1✔
438
                add := s.Changes[top]
1✔
439
                s.Changes = s.Changes[:top]
1✔
440
                s.Changes[top-1] = del + "\n" + add
1✔
441
        }
442

443
        // Check if insert position is between first and last line
444
        // of a block of ACl lines.
445
        // Returns action and blockID or empty action, if at border of block.
446
        insideBlock := func(pos int) (string, int) {
2✔
447
                var lowAct, highAct string
1✔
448
                var id int
1✔
449
                for i := pos - 1; i >= 0; i-- {
2✔
450
                        if a := getIOSAction(al[i]); a != "remark" {
2✔
451
                                lowAct = a
1✔
452
                                id = idx2Block[i]
1✔
453
                                break
1✔
454
                        }
455
                }
456
                for i := pos; i < len(al); i++ {
2✔
457
                        if a := getIOSAction(al[i]); a != "remark" {
2✔
458
                                highAct = a
1✔
459
                                id = idx2Block[i]
1✔
460
                                break
1✔
461
                        }
462
                }
463
                if lowAct == highAct {
2✔
464
                        return lowAct, id
1✔
465
                }
1✔
466
                return "", 0
1✔
467
        }
468
        for _, r := range diff {
2✔
469
                if r.IsInsert() {
2✔
470
                        // Check for dangerous case, where block is split into two blocks:
1✔
471
                        // - a deny rule is inserted into a block of permit rules or
1✔
472
                        // - a permit rule is inserted into a block of deny rules.
1✔
473
                        if action, id := insideBlock(r.LowA); action != "" {
2✔
474
                                for _, c := range bl[r.LowB:r.HighB] {
2✔
475
                                        if action != getIOSAction(c) {
2✔
476
                                                maxID++
1✔
477
                                                for i, id0 := range idx2Block[r.LowA:] {
2✔
478
                                                        if id0 != id {
2✔
479
                                                                break
1✔
480
                                                        }
481
                                                        idx2Block[r.LowA+i] = maxID
1✔
482
                                                }
483
                                                break
1✔
484
                                        }
485
                                }
486
                        }
487
                } else if r.IsDelete() {
2✔
488
                        for i, a := range al[r.LowA:r.HighA] {
2✔
489
                                p := getPrintableCmd(a, s.DeviceCfg)
1✔
490
                                p = stripLogRX.ReplaceAllLiteralString(p, "")
1✔
491
                                cmdPos := cmdAndPos{cmd: a, pos: r.LowA + i}
1✔
492
                                delMap[p] = &cmdPos
1✔
493
                                del = append(del, &cmdPos)
1✔
494
                        }
1✔
495
                }
496
        }
497
        // An ACL line which is already present on device can't be added again.
498
        // Therefore we need add, delete and move operations.
499
        //
500
        // Two ACL lines which differ only in 'log' attribute,
501
        // can't both be present on a device.
502
        // [ log | log-input ]
503
        // Hence we must remove one line before we can add the other one.
504
        //
505
        // A command is moved if the same command is deleted and added.
506
        // Two commands are equal if they
507
        // - have same attribute .parsed,
508
        // - but attribute 'log' is ignored during compare.
509
        for _, r := range diff {
2✔
510
                if r.IsInsert() {
2✔
511
                        if r.HighB-r.LowB >= 10000 {
2✔
512
                                errlog.Abort("Can't insert more than 9999 ACL lines at once")
1✔
513
                        }
1✔
514
                        action0 := getIOSAction(bl[r.LowB])
1✔
515
                        moveOK := true
1✔
516
                        for i, b := range bl[r.LowB:r.HighB] {
2✔
517
                                moveOK = moveOK && action0 == getIOSAction(b)
1✔
518
                                p := s.printNetspocCmd(b)
1✔
519
                                p = stripLogRX.ReplaceAllLiteralString(p, "")
1✔
520
                                if cmdPos, found := delMap[p]; found {
2✔
521
                                        moveACL(cmdPos, b, r.LowA, i, moveOK)
1✔
522
                                } else {
2✔
523
                                        addACL(b, r.LowA, i)
1✔
524
                                }
1✔
525
                        }
526
                }
527
        }
528
        // Delete lines on device.
529
        // Work from bottom to top. Otherwise we would permit too much
530
        // traffic for a short time range.
531
        slices.Reverse(del)
1✔
532
        for _, cmdPos := range del {
2✔
533
                // Must not delete cmd again, if it already was moved.
1✔
534
                if cmdPos.cmd != nil {
2✔
535
                        delACL(cmdPos)
1✔
536
                }
1✔
537
        }
538
        if len(s.Changes) == chgLen {
2✔
539
                // No changes found; remove initial resequence command.
1✔
540
                s.Changes = s.Changes[:chgLen-1]
1✔
541
        } else {
2✔
542
                s.addToplevel("ip access-list resequence " + aclName + " 10 10")
1✔
543
        }
1✔
544
}
545

546
// Mark rules belonging to block of successive rules with identical action,
547
// where order doesn't matter in this case.
548
// This allows to find rules as unchanged if only the order has changed.
549
// Action "remark" belongs to any block.
550
//
551
// Traffic from Netspoc is filtered by some rule.
552
// It is crucial to never move this rule, because this would lockout
553
// Netspoc from device. This is safeguarded by not moving rules inside
554
// the same block.
555
//
556
// Each block of rules is marked by an unique number.
557
// Returns a slice mapping rule position to block number.
558
func markIOSPermitDenyBlocks(l []*cmd) ([]int, int) {
1✔
559
        result := make([]int, len(l))
1✔
560
        blockID := 1
1✔
561
        action := ""
1✔
562
        for i, c := range l {
2✔
563
                a := getIOSAction(c)
1✔
564
                switch a {
1✔
565
                case action, "remark":
1✔
566
                default:
1✔
567
                        if action != "" {
2✔
568
                                blockID++
1✔
569
                        }
1✔
570
                        action = a
1✔
571
                }
572
                result[i] = blockID
1✔
573
        }
574
        return result, blockID
1✔
575
}
576

577
func getIOSAction(c *cmd) string {
1✔
578
        action, _, _ := strings.Cut(c.parsed, " ")
1✔
579
        return action
1✔
580
}
1✔
581

582
func (s *State) diffASAACLs(al, bl []*cmd, diff []edit.Range) {
1✔
583
        // Collect to be added and to be deleted entries.
1✔
584
        var add, del []*cmd
1✔
585
        // Position where to add or delete a command.
1✔
586
        pos := make(map[*cmd]int)
1✔
587

1✔
588
        // access-list NAME ...
1✔
589
        // ==>
1✔
590
        // access-list NAME line N ...
1✔
591
        insertLineNr := func(s string, pos int) string {
2✔
592
                nr := strconv.Itoa(pos + 1)
1✔
593
                l := len("access-list ")
1✔
594
                i := strings.Index(s[l:], " ")
1✔
595
                return s[:l+i] + " line " + nr + s[l+i:]
1✔
596
        }
1✔
597
        addACL := func(b *cmd) {
2✔
598
                b.parsed = insertLineNr(b.parsed, pos[b])
1✔
599
                s.addCmds([]*cmd{b})
1✔
600
                i := pos[b]
1✔
601
                for cmd, p := range pos {
2✔
602
                        if p >= i {
2✔
603
                                pos[cmd] = p + 1
1✔
604
                        }
1✔
605
                }
606
        }
607
        delACL := func(a *cmd) {
2✔
608
                a.orig = insertLineNr(a.orig, pos[a])
1✔
609
                s.delCmds([]*cmd{a})
1✔
610
                i := pos[a]
1✔
611
                for cmd, p := range pos {
2✔
612
                        if p > i {
2✔
613
                                pos[cmd] = p - 1
1✔
614
                        }
1✔
615
                }
616
        }
617

618
        // Generate move command which sends add and delete command together
619
        // as a single command.
620
        //
621
        // First append delete and add command to s.changes:
622
        // i  : no access-list ...
623
        // i+1: object-group g1 ...
624
        // ...: object-group gN ...
625
        // top: access-list ...
626
        //
627
        // Then
628
        // - move commands between 'i' and 'top' to position 'i',
629
        // - join commands at 'i' and at 'top' into a single command at position 'top'-1
630
        // i  : object-group g1 ...
631
        // ...: object-group gN ...
632
        // top-1: no access-list ...\n access-list
633
        moveACL := func(a, b *cmd) {
2✔
634
                delACL(a)
1✔
635
                i := len(s.Changes) - 1
1✔
636
                addACL(b)
1✔
637
                top := len(s.Changes) - 1
1✔
638
                del := s.Changes[i]
1✔
639
                add := s.Changes[top]
1✔
640
                copy(s.Changes[i:], s.Changes[i+1:])
1✔
641
                s.Changes = s.Changes[:top]
1✔
642
                s.Changes[top-1] = del + "\n" + add
1✔
643
        }
1✔
644

645
        // al and bl are list of access-list commands known to be equal,
646
        // assuming object-groups as equal.
647
        // Equalize corresponding object-groups.
648
        // Also add commands to change access-list that need to be changed on
649
        // device, because name of referenced object-group has changed.
650
        equalizeACLs := func(al, bl []*cmd, lowA int) {
2✔
651
                for i, a := range al {
2✔
652
                        b := bl[i]
1✔
653
                        b.name = a.name
1✔
654
                        pos[a] = lowA + i
1✔
655
                        pos[b] = lowA + i
1✔
656
                        changedRef := false
1✔
657
                        for i, aName := range a.ref {
2✔
658
                                bName := b.ref[i]
1✔
659
                                if !s.equalizedGroups(aName, bName) {
2✔
660
                                        changedRef = true
1✔
661
                                }
1✔
662
                        }
663
                        if changedRef {
2✔
664
                                add = append(add, b)
1✔
665
                                del = append(del, a)
1✔
666
                        } else {
2✔
667
                                a.needed = true
1✔
668
                                b.ready = true
1✔
669
                        }
1✔
670
                }
671
        }
672
        // Check for identical groups early and equalize groups later.
673
        for _, r := range diff {
2✔
674
                if r.IsInsert() {
2✔
675
                        for _, c := range bl[r.LowB:r.HighB] {
2✔
676
                                for _, bName := range c.ref {
2✔
677
                                        s.findGroupOnDevice(bName)
1✔
678
                                }
1✔
679
                        }
680
                }
681
        }
682
        for _, r := range diff {
2✔
683
                if r.IsInsert() {
2✔
684
                        for _, c := range bl[r.LowB:r.HighB] {
2✔
685
                                pos[c] = r.LowA
1✔
686
                        }
1✔
687
                        add = append(add, bl[r.LowB:r.HighB]...)
1✔
688
                } else if r.IsDelete() {
2✔
689
                        for i, c := range al[r.LowA:r.HighA] {
2✔
690
                                pos[c] = i + r.LowA
1✔
691
                        }
1✔
692
                        del = append(del, al[r.LowA:r.HighA]...)
1✔
693
                } else if r.IsEqual() {
2✔
694
                        equalizeACLs(al[r.LowA:r.HighA], bl[r.LowB:r.HighB], r.LowA)
1✔
695
                }
1✔
696
        }
697
        // An ACL line which is already present on device can't be added again.
698
        // Therefore we need add, delete and move operations.
699
        //
700
        // Two ACL lines which differ only in 'log' attribute,
701
        // can't both be present on a device.
702
        // [ log [ [ level ] interval secs ] | disable | default ] ]
703
        // [ time-range time_range_name ] [ inactive ]
704
        // Hence we must remove one line before we can add the other one.
705
        //
706
        // Find move operations from commands in 'add' and 'del'.
707
        // A command is moved if the same command is deleted and added.
708
        // Two commands are equal if they
709
        // - have same attribute .parsed,
710
        //   but attribute 'log' is ignored during compare,
711
        // - reference the same object-groups. Here we must compare
712
        //   object-groups from Netspoc with object-groups on device.
713
        delMap := make(map[string]*cmd)
1✔
714
        rx := regexp.MustCompile(
1✔
715
                ` log( ((\w+ )?interval \d+|\w+|disable|default))?\b`)
1✔
716
        for _, a := range del {
2✔
717
                p := getPrintableCmd(a, s.DeviceCfg)
1✔
718
                p = rx.ReplaceAllLiteralString(p, "")
1✔
719
                delMap[p] = a
1✔
720
        }
1✔
721
        for _, b := range add {
2✔
722
                p := s.printNetspocCmd(b)
1✔
723
                p = rx.ReplaceAllLiteralString(p, "")
1✔
724
                if a := delMap[p]; a != nil {
2✔
725
                        moveACL(a, b)
1✔
726
                        continue
1✔
727
                }
728
                addACL(b)
1✔
729
        }
730
        // Delete lines on device.
731
        // Work from bottom to top. Otherwise we would permit too much
732
        // traffic for a short time range.
733
        slices.Reverse(del)
1✔
734
        for _, a := range del {
2✔
735
                // Must not delete cmd again, if it already was moved.
1✔
736
                if !a.needed {
2✔
737
                        delACL(a)
1✔
738
                }
1✔
739
        }
740
}
741

742
// ga and gb are object-group commands that need to be equal.
743
// Try to transform subcommands of ga to subcommands of gb if viable.
744
// Return true if transformation succeeded.
745
func (s *State) equalizedGroups(aName, bName string) bool {
1✔
746
        ga := s.DeviceCfg.lookup["object-group"][aName][0]
1✔
747
        gb := s.SpocCfg.lookup["object-group"][bName][0]
1✔
748
        if ga.parsed != gb.parsed {
2✔
749
                // Type of object-group differs.
1✔
750
                return false
1✔
751
        }
1✔
752
        if ga.needed {
2✔
753
                if gb.ready {
2✔
754
                        return ga.name == gb.name
1✔
755
                }
1✔
756
                s.findGroupOnDevice(bName)
1✔
757
                return false
1✔
758
        }
759
        la := ga.sub
1✔
760
        lb := gb.sub
1✔
761
        byOrig := func(_ *Config, c *cmd) string { return c.orig }
2✔
762
        ab := &cmdsPair{aCmds: la, bCmds: lb, key: byOrig}
1✔
763
        script := myers.Diff(nil, ab)
1✔
764
        if !script.IsIdentity() {
2✔
765
                s.findGroupOnDevice(bName)
1✔
766
                if gb.ready {
2✔
767
                        return aName == gb.name
1✔
768
                }
1✔
769
        }
770
        ins, del := script.Stat()
1✔
771
        if ins+del > len(lb) {
2✔
772
                return false
1✔
773
        }
1✔
774
        ga.needed = true
1✔
775
        gb.name = ga.name
1✔
776
        for _, r := range script.Ranges {
2✔
777
                if r.IsDelete() {
2✔
778
                        s.delCmds(la[r.LowA:r.HighA])
1✔
779
                } else if r.IsInsert() {
3✔
780
                        s.addCmds(lb[r.LowB:r.HighB])
1✔
781
                }
1✔
782
        }
783
        gb.ready = true
1✔
784
        return true
1✔
785
}
786

787
type routeDst struct {
788
        vrf string
789
        dst netip.Prefix
790
}
791

792
func (s *State) diffRoutes(al, bl []*cmd, diff []edit.Range) {
1✔
793
        chgVRF := make(map[string]bool)
1✔
794
        for _, c := range bl {
2✔
795
                chgVRF[dstOfRoute(c).vrf] = true
1✔
796
        }
1✔
797
        delDst := make(map[routeDst]*cmd)
1✔
798
        for _, r := range diff {
2✔
799
                if r.IsDelete() {
2✔
800
                        for _, c := range al[r.LowA:r.HighA] {
2✔
801
                                delDst[dstOfRoute(c)] = c
1✔
802
                        }
1✔
803
                }
804
        }
805
        for _, r := range diff {
2✔
806
                if r.IsInsert() {
2✔
807
                        for _, c := range bl[r.LowB:r.HighB] {
2✔
808
                                add := s.printNetspocCmd(c)
1✔
809
                                c.ready = true
1✔
810
                                if del, found := delDst[dstOfRoute(c)]; found {
2✔
811
                                        // ASA doesn't allow two routes to identical
1✔
812
                                        // destination. Remove and add routes in one transaction.
1✔
813
                                        s.addToplevel("no " + del.orig + "\n" + add)
1✔
814
                                        del.needed = true
1✔
815
                                } else {
2✔
816
                                        s.addToplevel(add)
1✔
817
                                }
1✔
818
                        }
819
                }
820
        }
821
        seenVRF := make(map[string]bool)
1✔
822
        for _, r := range diff {
2✔
823
                if r.IsDelete() {
2✔
824
                        for _, c := range al[r.LowA:r.HighA] {
2✔
825
                                ipv := "IPv4"
1✔
826
                                if strings.Contains(c.parsed, "ipv6") {
2✔
827
                                        ipv = "ipv6"
1✔
828
                                }
1✔
829
                                if vrf := dstOfRoute(c).vrf; chgVRF[vrf] {
2✔
830
                                        s.delCmds([]*cmd{c})
1✔
831
                                } else if !seenVRF[ipv+vrf] {
3✔
832
                                        seenVRF[ipv+vrf] = true
1✔
833
                                        forVRF := ""
1✔
834
                                        if vrf != "" {
2✔
835
                                                forVRF = " for VRF " + vrf
1✔
836
                                        }
1✔
837
                                        errlog.Info("No %s routing specified%s, leaving untouched",
1✔
838
                                                ipv, forVRF)
1✔
839
                                }
840
                        }
841
                }
842
        }
843
}
844

845
// Recursively transfer commands referenced from command and subcommands.
846
// Then transfer command and its subcommands.
847
// Mark transferred commands.
848
// Transfer each command only once.
849
func (s *State) addCmds(l []*cmd) {
1✔
850
        var add func(l []*cmd)
1✔
851
        follow := func(c *cmd) {
2✔
852
                for i, name := range c.ref {
2✔
853
                        prefix := c.typ.ref[i]
1✔
854
                        bl := s.SpocCfg.lookup[prefix][name]
1✔
855
                        if bl[0].typ.fixedName || bl[0].fixedName {
2✔
856
                                if al, found := s.DeviceCfg.lookup[prefix][name]; found {
2✔
857
                                        if prefix == "crypto map" {
2✔
858
                                                s.diffCryptoMap(al, bl)
1✔
859
                                        } else {
2✔
860
                                                s.diffCmds(al, bl, byParsedCmd)
1✔
861
                                        }
1✔
862
                                        continue
1✔
863
                                }
864
                        }
865
                        add(bl)
1✔
866
                }
867
        }
868
        add = func(bl []*cmd) {
2✔
869
                b0 := bl[0]
1✔
870
                if b0.ready {
2✔
871
                        return
1✔
872
                }
1✔
873
                b0.ready = true
1✔
874
                if b0.typ.simpleObj {
2✔
875
                        if al := s.findSimpleObjOnDevice(bl); al != nil {
2✔
876
                                al[0].needed = true
1✔
877
                                b0.name = al[0].name
1✔
878
                                return
1✔
879
                        }
1✔
880
                }
881
                if b0.typ.fixedName || b0.fixedName {
2✔
882
                        prefix := b0.typ.prefix
1✔
883
                        name := b0.name
1✔
884
                        if _, found := s.DeviceCfg.lookup[prefix][name]; !found {
2✔
885
                                switch prefix {
1✔
886
                                case "aaa-server", "ldap attribute-map":
1✔
887
                                        errlog.Abort("'%s %s' must be transferred manually", prefix, name)
1✔
888
                                }
889
                        }
890
                }
891
                for _, c := range bl {
2✔
892
                        follow(c)
1✔
893
                        for _, sc := range c.sub {
2✔
894
                                follow(sc)
1✔
895
                        }
1✔
896
                        s.addCmd(c)
1✔
897
                }
898
        }
899
        add(l)
1✔
900
}
901

902
func (s *State) addCmd(c *cmd) {
1✔
903
        switch c.typ.prefix {
1✔
904
        case "aaa-server", "ldap attribute-map", "interface":
1✔
905
                return
1✔
906
        }
907
        pr := s.printNetspocCmd(c)
1✔
908
        // If current command is subcommand of some command x
1✔
909
        // then write x and remember that x has been written,
1✔
910
        // so that it isn't written again if another subcommand of x is modified.
1✔
911
        if sup := c.subCmdOf; sup != nil {
2✔
912
                pr2 := s.printNetspocCmd(sup)
1✔
913
                s.setCmdConfMode(pr2)
1✔
914
        } else if c.typ.sub != nil {
3✔
915
                s.subCmdOf = pr
1✔
916
        } else {
2✔
917
                s.subCmdOf = ""
1✔
918
        }
1✔
919
        s.addChange(pr)
1✔
920
        for _, sub := range c.sub {
2✔
921
                s.addChange(s.printNetspocCmd(sub))
1✔
922
        }
1✔
923
}
924

925
func (s *State) addToplevel(c string) {
1✔
926
        s.addChange(c)
1✔
927
        s.subCmdOf = ""
1✔
928
}
1✔
929

930
func (s *State) setCmdConfMode(printedSup string) {
1✔
931
        if s.subCmdOf != printedSup {
2✔
932
                // Prevent toplevel command "webvpn" be given
1✔
933
                // in mode (config-group-policy)
1✔
934
                // since this mode also has a subcommand "webvpn"
1✔
935
                if s.subCmdOf != "" {
2✔
936
                        s.addChange("exit")
1✔
937
                }
1✔
938
                s.addChange(printedSup)
1✔
939
                s.subCmdOf = printedSup
1✔
940
        }
941
}
942

943
// Delete subcommands and parts of multi line command.
944
func (s *State) delCmds(l []*cmd) {
1✔
945
        if len(l) == 0 {
2✔
946
                return
1✔
947
        }
1✔
948
        switch l[0].typ.prefix {
1✔
949
        // Leave these commands unchanged on device:
950
        case "interface":
1✔
951
                return
1✔
952
        }
953
        for _, c := range l {
2✔
954
                if c.needed {
2✔
955
                        continue
1✔
956
                }
957
                if sup := c.subCmdOf; sup != nil {
2✔
958
                        s.setCmdConfMode(sup.orig)
1✔
959
                } else {
2✔
960
                        s.subCmdOf = ""
1✔
961
                }
1✔
962
                c.needed = true // Don't delete again later.
1✔
963
                s.addChange("no " + c.orig)
1✔
964
        }
965
        // Mark referenced commands as deleted.
966
        s.markDeleted(l)
1✔
967
}
968

969
// Mark command on device and referenced commands as deleted.
970
func (s *State) markDeleted(al []*cmd) {
1✔
971
        var del func(al []*cmd)
1✔
972
        follow := func(c *cmd) {
2✔
973
                for i, name := range c.ref {
2✔
974
                        prefix := c.typ.ref[i]
1✔
975
                        del(s.DeviceCfg.lookup[prefix][name])
1✔
976
                }
1✔
977
        }
978
        del = func(al []*cmd) {
2✔
979
                switch al[0].typ.prefix {
1✔
980
                // Leave these commands unchanged on device:
981
                case "aaa-server", "ldap attribute-map", "interface":
1✔
982
                        return
1✔
983
                }
984
                for _, c := range al {
2✔
985
                        if !c.toDelete {
2✔
986
                                c.toDelete = true
1✔
987
                                follow(c)
1✔
988
                                for _, sc := range c.sub {
2✔
989
                                        follow(sc)
1✔
990
                                }
1✔
991
                        }
992
                }
993
        }
994
        del(al)
1✔
995
}
996

997
// Delete unused toplevel command from device
998
// - that was previously referenced by command generated from Netspoc or
999
// - that was generated by Netspoc, but was accidently not deleted in last run.
1000
func (s *State) deleteUnused() {
1✔
1001
        // Collect to be deleted commands.
1✔
1002
        // That are unused, no longer needed commands.
1✔
1003
        type pair [2]string
1✔
1004
        toDelete := make(map[pair][]*cmd)
1✔
1005
        stillReferenced := make(map[pair]bool)
1✔
1006
        for prefix, m := range s.DeviceCfg.lookup {
2✔
1007
                for name, l := range m {
2✔
1008
                        var del []*cmd
1✔
1009
                        for _, c := range l {
2✔
1010
                                if !c.needed {
2✔
1011
                                        if c.toDelete || strings.Contains(c.name, "-DRC-") {
2✔
1012
                                                del = append(del, c)
1✔
1013
                                        } else {
2✔
1014
                                                // Leave an unused command unchanged,
1✔
1015
                                                // if its name doesn't contain "-DRC-".
1✔
1016
                                                // Because then it initially wasn't created by Netspoc.
1✔
1017
                                                // Also leave commands unchanged,
1✔
1018
                                                // that are still referenced by such commands.
1✔
1019
                                                var follow func(c *cmd)
1✔
1020
                                                follow = func(c *cmd) {
2✔
1021
                                                        for i, name := range c.ref {
2✔
1022
                                                                prefix := c.typ.ref[i]
1✔
1023
                                                                for _, c2 := range s.DeviceCfg.lookup[prefix][name] {
2✔
1024
                                                                        if !c2.needed {
2✔
1025
                                                                                stillReferenced[pair{prefix, name}] = true
1✔
1026
                                                                                follow(c2)
1✔
1027
                                                                        }
1✔
1028
                                                                }
1029
                                                        }
1030
                                                        for _, c2 := range c.sub {
2✔
1031
                                                                if !c2.needed {
2✔
1032
                                                                        follow(c2)
1✔
1033
                                                                }
1✔
1034
                                                        }
1035
                                                }
1036
                                                follow(c)
1✔
1037
                                        }
1038
                                }
1039
                        }
1040
                        if del != nil {
2✔
1041
                                toDelete[pair{prefix, name}] = del
1✔
1042
                        }
1✔
1043
                }
1044
        }
1045
        if len(stillReferenced) > 0 {
2✔
1046
                for p := range toDelete {
2✔
1047
                        if stillReferenced[p] {
2✔
1048
                                delete(toDelete, p)
1✔
1049
                        }
1✔
1050
                }
1051
        }
1052
        if len(toDelete) > 0 && s.subCmdOf != "" {
2✔
1053
                s.addChange("exit")
1✔
1054
        }
1✔
1055
        for len(toDelete) > 0 {
2✔
1056
                // Mark commands that are still referenced by other to be
1✔
1057
                // deleted commands. Delete them afterwards.
1✔
1058
                isReferenced := make(map[pair]bool)
1✔
1059
                follow := func(c *cmd) {
2✔
1060
                        for i, name := range c.ref {
2✔
1061
                                prefix := c.typ.ref[i]
1✔
1062
                                isReferenced[pair{prefix, name}] = true
1✔
1063
                        }
1✔
1064
                }
1065
                for _, l := range toDelete {
2✔
1066
                        for _, c := range l {
2✔
1067
                                follow(c)
1✔
1068
                                for _, sc := range c.sub {
2✔
1069
                                        follow(sc)
1✔
1070
                                }
1✔
1071
                        }
1072
                }
1073
                // Delete commands not referenced any longer.
1074
                pairs := slices.SortedFunc(maps.Keys(toDelete), func(a, b pair) int {
2✔
1075
                        return cmp.Or(cmp.Compare(a[0], b[0]), cmp.Compare(a[1], b[1]))
1✔
1076
                })
1✔
1077
                for _, pair := range pairs {
2✔
1078
                        if isReferenced[pair] {
2✔
1079
                                continue
1✔
1080
                        }
1081
                        prefix := pair[0]
1✔
1082
                        l := toDelete[pair]
1✔
1083
                        delete(toDelete, pair)
1✔
1084
                        if l[0].typ.clearConf {
2✔
1085
                                name := pair[1]
1✔
1086
                                s.addToplevel("clear configure " + prefix + " " + name)
1✔
1087
                        } else {
2✔
1088
                                for _, c := range l {
2✔
1089
                                        if c2, found := strings.CutPrefix(c.orig, "no "); found {
2✔
1090
                                                s.addToplevel(c2)
1✔
1091
                                        } else {
2✔
1092
                                                s.addToplevel("no " + c.orig)
1✔
1093
                                        }
1✔
1094
                                }
1095
                        }
1096
                }
1097
        }
1098
}
1099

1100
func (s *State) printNetspocCmd(c *cmd) string {
1✔
1101
        return getPrintableCmd(c, s.SpocCfg)
1✔
1102
}
1✔
1103

1104
func getPrintableCmd(c *cmd, cf *Config) string {
1✔
1105
        p := c.parsed
1✔
1106
        p = strings.Replace(p, "$NAME", c.name, 1)
1✔
1107
        p = strings.Replace(p, "$SEQ", strconv.Itoa(c.seq), 1)
1✔
1108
        for i, r := range c.ref {
2✔
1109
                prefix := c.typ.ref[i]
1✔
1110
                name := cf.lookup[prefix][r][0].name
1✔
1111
                p = strings.Replace(p, "$REF", name, 1)
1✔
1112
        }
1✔
1113
        return p
1✔
1114
}
1115

1116
// Generate new names for objects from Netspoc: <spoc-name>-DRC-<index>
1117
func (s *State) generateNamesForTransfer() {
1✔
1118
        setName := func(c *cmd, devNames map[string][]*cmd) {
2✔
1119
                prefix := c.name + "-DRC-"
1✔
1120
                index := 0
1✔
1121
                for {
2✔
1122
                        newName := prefix + strconv.Itoa(index)
1✔
1123
                        if _, found := devNames[newName]; !found {
2✔
1124
                                c.name = newName
1✔
1125
                                break
1✔
1126
                        }
1127
                        index++
1✔
1128
                }
1129
        }
1130
        for prefix, m := range s.SpocCfg.lookup {
2✔
1131
                for _, bl := range m {
2✔
1132
                        for _, c := range bl {
2✔
1133
                                if !(c.typ.fixedName || c.fixedName) {
2✔
1134
                                        setName(c, s.DeviceCfg.lookup[prefix])
1✔
1135
                                }
1✔
1136
                        }
1137
                }
1138
        }
1139
}
1140

1141
// Sort elements of object-groups for findGroupOnDevice to work.
1142
func sortGroups(cf *Config) {
1✔
1143
        for _, gl := range cf.lookup["object-group"] {
2✔
1144
                l := gl[0].sub
1✔
1145
                sort.Slice(l, func(i, j int) bool { return l[i].parsed < l[j].parsed })
2✔
1146
        }
1147
}
1148

1149
func (s *State) findGroupOnDevice(name string) {
1✔
1150
        gb := s.SpocCfg.lookup["object-group"][name][0]
1✔
1151
        if gb.ready {
2✔
1152
                return
1✔
1153
        }
1✔
1154
GROUP:
1✔
1155
        for _, l := range s.DeviceCfg.lookup["object-group"] {
2✔
1156
                ga := l[0]
1✔
1157
                if ga.parsed != gb.parsed {
2✔
1158
                        // Type of object-group differs.
1✔
1159
                        continue
1✔
1160
                }
1161
                // Group ga was already changed to elements of some group from
1162
                // Netspoc or it is equal to some group from Netspoc.
1163
                if ga.needed {
2✔
1164
                        continue
1✔
1165
                }
1166
                if len(ga.sub) != len(gb.sub) {
2✔
1167
                        continue
1✔
1168
                }
1169
                for i, n := range gb.sub {
2✔
1170
                        if n.orig != ga.sub[i].orig {
2✔
1171
                                continue GROUP
1✔
1172
                        }
1173
                }
1174
                ga.needed = true
1✔
1175
                gb.ready = true
1✔
1176
                gb.name = ga.name
1✔
1177
                break
1✔
1178
        }
1179
}
1180

1181
func (s *State) diffCryptoMap(al, bl []*cmd) string {
1✔
1182
        matchCryptoMap(al, bl, func(aSeqL, bSeqL []*cmd) {
2✔
1183
                s.diffCmds(aSeqL, bSeqL, byParsedCmd)
1✔
1184
        })
1✔
1185
        return al[0].name
1✔
1186
}
1187

1188
func matchCryptoMap(al, bl []*cmd, f func([]*cmd, []*cmd)) {
1✔
1189
        mapBySeq := func(l []*cmd) map[int][]*cmd {
2✔
1190
                m := make(map[int][]*cmd)
1✔
1191
                for _, c := range l {
2✔
1192
                        m[c.seq] = append(m[c.seq], c)
1✔
1193
                }
1✔
1194
                return m
1✔
1195
        }
1196
        getPeer := func(l []*cmd) string {
2✔
1197
                name, seq := l[0].name, l[0].seq
1✔
1198
                // For IOS look into subcommands.
1✔
1199
                if len(l) == 1 && l[0].sub != nil {
2✔
1200
                        l = l[0].sub
1✔
1201
                }
1✔
1202
                peer := ""
1✔
1203
                for _, c := range l {
2✔
1204
                        if _, p, found := strings.Cut(c.parsed, "set peer "); found {
2✔
1205
                                peer = "peer " + p
1✔
1206
                                break
1✔
1207
                        }
1208
                        if strings.Contains(c.parsed, "ipsec-isakmp dynamic ") {
2✔
1209
                                peer = c.ref[0]
1✔
1210
                                break
1✔
1211
                        }
1212
                }
1213
                if peer == "" {
2✔
1214
                        errlog.Abort("Missing peer or dynamic in crypto map %s %d", name, seq)
1✔
1215
                }
1✔
1216
                return peer
1✔
1217
        }
1218
        mapPeerToSeq := func(seqMap map[int][]*cmd) map[string]int {
2✔
1219
                m := make(map[string]int)
1✔
1220
                for seq, l := range seqMap {
2✔
1221
                        m[getPeer(l)] = seq
1✔
1222
                }
1✔
1223
                return m
1✔
1224
        }
1225

1226
        aSeqMap := mapBySeq(al)
1✔
1227
        bSeqMap := mapBySeq(bl)
1✔
1228
        bPeer2Seq := mapPeerToSeq(bSeqMap)
1✔
1229
        // Match commands having same peer.
1✔
1230
        for _, aSeq := range slices.Sorted(maps.Keys(aSeqMap)) {
2✔
1231
                aSeqL := aSeqMap[aSeq]
1✔
1232
                aPeer := getPeer(aSeqL)
1✔
1233
                if bSeq, found := bPeer2Seq[aPeer]; found {
2✔
1234
                        f(aSeqL, bSeqMap[bSeq])
1✔
1235
                        delete(bSeqMap, bSeq) // Mark as already processed.
1✔
1236
                } else {
2✔
1237
                        f(aSeqL, nil)
1✔
1238
                }
1✔
1239
        }
1240
        // Use fresh sequence numbers for added commands.
1241
        static := 1
1✔
1242
        dynamic := 65535
1✔
1243
        for _, bSeq := range slices.Sorted(maps.Keys(bSeqMap)) {
2✔
1244
                bSeqL := bSeqMap[bSeq]
1✔
1245
                seq := &dynamic
1✔
1246
                incr := -1
1✔
1247
                if strings.HasPrefix(getPeer(bSeqL), "peer ") {
2✔
1248
                        seq = &static
1✔
1249
                        incr = 1
1✔
1250
                }
1✔
1251
                // Get next free seq num.
1252
                for ; aSeqMap[*seq] != nil; *seq += incr {
2✔
1253
                }
1✔
1254
                for _, bCmd := range bSeqL {
2✔
1255
                        bCmd.seq = *seq
1✔
1256
                        // Use name of existing crypto map.
1✔
1257
                        if len(al) > 0 {
2✔
1258
                                bCmd.name = al[0].name
1✔
1259
                        }
1✔
1260
                }
1261
                f(nil, bSeqL)
1✔
1262
                *seq += incr
1✔
1263
        }
1264
}
1265

1266
// Add routes with long mask first. If we switch the default
1267
// route, this ensures, that we have the new routes available
1268
// before deleting the old default route.
1269
func sortRoutes(cf *Config) {
1✔
1270
        for _, prefix := range []string{"route", "ip route", "ipv6 route"} {
2✔
1271
                l := cf.lookup[prefix][""]
1✔
1272
                sort.Slice(l, func(i, j int) bool {
2✔
1273
                        return byMoreSpecificRoute(l[i]) < byMoreSpecificRoute(l[j])
1✔
1274
                })
1✔
1275
        }
1276
}
1277

1278
func byMoreSpecificRoute(c *cmd) string {
1✔
1279
        // .Bits() has result -1, if .dst is invalid.
1✔
1280
        b := 128 - byte(dstOfRoute(c).dst.Bits())
1✔
1281
        return string([]byte{b}) + c.parsed
1✔
1282
}
1✔
1283

1284
func dstOfRoute(c *cmd) routeDst {
1✔
1285
        vrf := ""
1✔
1286
        var ipp netip.Prefix
1✔
1287
        l := strings.Split(c.parsed, " ")
1✔
1288
        if c.typ.prefix == "ipv6 route" {
2✔
1289
                // ASA: ipv6 route intf ip/len gw
1✔
1290
                // IOS: ipv6 route [vrf NAME] ip/len gw
1✔
1291
                i := slices.IndexFunc(l, func(e string) bool { return strings.Contains(e, "/") })
2✔
1292
                ipp, _ = netip.ParsePrefix(l[i])
1✔
1293
                if len(l) >= 6 && l[2] == "vrf" {
1✔
1294
                        vrf = l[3]
×
1295
                }
×
1296
        } else {
1✔
1297
                // ASA: route intf ip mask gw
1✔
1298
                // IOS: ip route [vrf NAME] ip mask gw
1✔
1299
                i := 2
1✔
1300
                if l[0] == "ip" && l[2] == "vrf" {
2✔
1301
                        vrf = l[3]
1✔
1302
                        i = 4
1✔
1303
                }
1✔
1304
                ip, err1 := netip.ParseAddr(l[i])
1✔
1305
                mask, err2 := netip.ParseAddr(l[i+1])
1✔
1306
                if err1 == nil && err2 == nil {
2✔
1307
                        size, _ := net.IPMask(mask.AsSlice()).Size()
1✔
1308
                        ipp = netip.PrefixFrom(ip, size)
1✔
1309
                }
1✔
1310
        }
1311
        return routeDst{vrf, ipp}
1✔
1312
}
1313

1314
func diffCmdLists(ab *cmdsPair) []edit.Range {
1✔
1315
        al := ab.aCmds
1✔
1316
        if len(al) > 0 {
2✔
1317
                if al[0].typ.prefix == "access-list" {
2✔
1318
                        return myers.Diff(nil, ab).Ranges
1✔
1319
                } else {
2✔
1320
                        s := al[0].subCmdOf
1✔
1321
                        if s != nil && s.typ.prefix == "ip access-list extended" {
2✔
1322
                                return myers.Diff(nil, ab).Ranges
1✔
1323
                        }
1✔
1324
                }
1325
        }
1326
        return diffUnordered(ab)
1✔
1327
}
1328

1329
func diffUnordered(ab *cmdsPair) []edit.Range {
1✔
1330
        var result []edit.Range
1✔
1331
        prev := &edit.Range{LowA: -1, HighA: -1, LowB: -1, HighB: -1}
1✔
1332
        m := make(map[string]int)
1✔
1333
        for i, bCmd := range ab.bCmds {
2✔
1334
                m[ab.key(ab.b, bCmd)] = i
1✔
1335
        }
1✔
1336
        for i, aCmd := range ab.aCmds {
2✔
1337
                k := ab.key(ab.a, aCmd)
1✔
1338
                if j, found := m[k]; found && j != -1 {
2✔
1339
                        if prev.IsEqual() && prev.HighA == i && prev.HighB == j {
2✔
1340
                                prev.HighA = i + 1
1✔
1341
                                prev.HighB = j + 1
1✔
1342
                        } else {
2✔
1343
                                result = append(result,
1✔
1344
                                        edit.Range{LowA: i, HighA: i + 1, LowB: j, HighB: j + 1})
1✔
1345
                                prev = &result[len(result)-1]
1✔
1346
                        }
1✔
1347
                        m[k] = -1
1✔
1348
                } else {
1✔
1349
                        if prev.IsDelete() && prev.HighA == i {
2✔
1350
                                prev.HighA = i + 1
1✔
1351
                        } else {
2✔
1352
                                result = append(result,
1✔
1353
                                        edit.Range{LowA: i, HighA: i + 1, LowB: 0, HighB: 0})
1✔
1354
                                prev = &result[len(result)-1]
1✔
1355
                        }
1✔
1356
                }
1357
        }
1358
        for j, bCmd := range ab.bCmds {
2✔
1359
                if m[ab.key(ab.b, bCmd)] != -1 {
2✔
1360
                        if prev.IsInsert() && prev.HighB == j {
2✔
1361
                                prev.HighB = j + 1
1✔
1362
                        } else {
2✔
1363
                                l := len(ab.aCmds)
1✔
1364
                                result = append(result,
1✔
1365
                                        edit.Range{LowA: l, HighA: l, LowB: j, HighB: j + 1})
1✔
1366
                                prev = &result[len(result)-1]
1✔
1367
                        }
1✔
1368
                }
1369
        }
1370
        return result
1✔
1371
}
1372

1373
func (s *State) checkInterfaces() error {
1✔
1374
        if s.Model == "IOS" {
2✔
1375
                return s.checkIOSInterfaces()
1✔
1376
        }
1✔
1377
        return s.checkASAInterfaces()
1✔
1378
}
1379

1380
func (s *State) checkASAInterfaces() error {
1✔
1381
        // For ASA collect implicit interfaces from Netspoc.
1✔
1382
        // These are defined by commands
1✔
1383
        // - "access-group $access-list in|out interface INTF".
1✔
1384
        // - "crypto map $crypto_map interface INTF"
1✔
1385
        // Ignore "access-group $access-list global".
1✔
1386
        getImplicitInterfaces := func(cfg *Config) map[string][]*cmd {
2✔
1387
                m := make(map[string][]*cmd)
1✔
1388
                for _, c := range cfg.lookup["access-group"][""] {
2✔
1389
                        tokens := strings.Fields(c.parsed)
1✔
1390
                        if len(tokens) == 5 {
2✔
1391
                                m[tokens[4]] = []*cmd{c}
1✔
1392
                        }
1✔
1393
                }
1394
                for _, c := range cfg.lookup["crypto map interface"][""] {
2✔
1395
                        intfName := strings.Fields(c.parsed)[4]
1✔
1396
                        m[intfName] = append(m[intfName], c)
1✔
1397
                }
1✔
1398
                return m
1✔
1399
        }
1400
        bIntf2cmd := getImplicitInterfaces(s.SpocCfg)
1✔
1401

1✔
1402
        // Collect and check named interfaces from device.
1✔
1403
        // Add implicit interfaces when comparing two Netspoc generated configs.
1✔
1404
        aIntf2cmd := getImplicitInterfaces(s.DeviceCfg)
1✔
1405
        for _, c := range s.DeviceCfg.lookup["interface"][""] {
2✔
1406
                name := ""
1✔
1407
                shut := false
1✔
1408
                for _, sc := range c.sub {
2✔
1409
                        tokens := strings.Fields(sc.parsed)
1✔
1410
                        switch tokens[0] {
1✔
1411
                        case "shutdown":
1✔
1412
                                shut = true
1✔
1413
                        case "nameif":
1✔
1414
                                name = tokens[1]
1✔
1415
                        }
1416
                }
1417
                if name != "" {
2✔
1418
                        if _, found := bIntf2cmd[name]; !found {
2✔
1419
                                // If some ACL or crypto map is bound to this unmanaged
1✔
1420
                                // interface, these commands must not accidently be deleted.
1✔
1421
                                s.markNeeded(aIntf2cmd[name])
1✔
1422

1✔
1423
                                if !shut {
2✔
1424
                                        errlog.Warning(
1✔
1425
                                                "Interface '%s' on device is not known by Netspoc", name)
1✔
1426
                                }
1✔
1427
                        }
1428
                        // Add map key, even if no commands are bound to this interface.
1429
                        aIntf2cmd[name] = nil
1✔
1430
                }
1431
        }
1432

1433
        // Check interfaces from Netspoc
1434
        for _, name := range slices.Sorted(maps.Keys(bIntf2cmd)) {
2✔
1435
                if _, found := aIntf2cmd[name]; !found {
2✔
1436
                        return fmt.Errorf(
1✔
1437
                                "Interface '%s' from Netspoc not known on device", name)
1✔
1438
                }
1✔
1439
        }
1440
        return nil
1✔
1441
}
1442

1443
func (s *State) checkIOSInterfaces() error {
1✔
1444
        type intfInfo struct {
1✔
1445
                shut    bool
1✔
1446
                addr    string
1✔
1447
                inspect string
1✔
1448
                vrf     string
1✔
1449
        }
1✔
1450
        // Read and remove sub commands from interface, that must not be
1✔
1451
        // compared later.
1✔
1452
        extractIntfInfo := func(c *cmd) *intfInfo {
2✔
1453
                info := &intfInfo{
1✔
1454
                        shut:    false,
1✔
1455
                        addr:    "",
1✔
1456
                        inspect: "disabled",
1✔
1457
                        vrf:     "<global>",
1✔
1458
                }
1✔
1459
                addrList := []string{}
1✔
1460
                l := c.sub
1✔
1461
                j := 0
1✔
1462
                for _, sc := range l {
2✔
1463
                        p := sc.parsed
1✔
1464
                        if p == "shutdown" {
2✔
1465
                                info.shut = true
1✔
1466
                        } else if ip, ok := strings.CutPrefix(p, "ip address "); ok {
3✔
1467
                                ip = strings.TrimSuffix(ip, " secondary")
1✔
1468
                                addrList = append(addrList, ip)
1✔
1469
                        } else if strings.HasPrefix(p, "ip unnumbered") {
3✔
1470
                                addrList = append(addrList, "unnumbered")
1✔
1471
                        } else if _, v, ok := strings.Cut(p, "vrf forwarding "); ok {
3✔
1472
                                info.vrf = v
1✔
1473
                        } else if strings.HasPrefix(p, "ip inspect") {
3✔
1474
                                info.inspect = "enabled"
1✔
1475
                        } else {
2✔
1476
                                // Only leave other commands as sub command.
1✔
1477
                                l[j] = sc
1✔
1478
                                j++
1✔
1479
                        }
1✔
1480
                }
1481
                c.sub = l[:j]
1✔
1482
                sort.Strings(addrList)
1✔
1483
                info.addr = strings.Join(addrList, ",")
1✔
1484
                return info
1✔
1485
        }
1486
        aKnown := make(map[string]bool)
1✔
1487
        bIntf := make(map[string]*intfInfo)
1✔
1488
        for _, c := range s.SpocCfg.lookup["interface"][""] {
2✔
1489
                name := strings.Fields(c.parsed)[1]
1✔
1490
                bIntf[name] = extractIntfInfo(c)
1✔
1491
        }
1✔
1492
        for _, c := range s.DeviceCfg.lookup["interface"][""] {
2✔
1493
                name := strings.Fields(c.parsed)[1]
1✔
1494
                aInfo := extractIntfInfo(c)
1✔
1495
                aKnown[name] = true
1✔
1496
                if bInfo := bIntf[name]; bInfo != nil {
2✔
1497
                        if aInfo.addr != bInfo.addr && bInfo.addr != "negotiated" {
2✔
1498
                                errlog.Warning(
1✔
1499
                                        "Different address defined for interface %s:"+
1✔
1500
                                                " Device: %q, Netspoc: %q", name, aInfo.addr, bInfo.addr)
1✔
1501
                        }
1✔
1502
                        if aInfo.inspect != bInfo.inspect {
2✔
1503
                                return fmt.Errorf(
1✔
1504
                                        "Different 'ip inspect' defined for interface %s:"+
1✔
1505
                                                " Device: %s, Netspoc: %s",
1✔
1506
                                        name, aInfo.inspect, bInfo.inspect)
1✔
1507
                        }
1✔
1508
                        if aInfo.vrf != bInfo.vrf {
2✔
1509
                                return fmt.Errorf(
1✔
1510
                                        "Different VRFs defined for interface %s:"+
1✔
1511
                                                " Device: %s, Netspoc: %s", name, aInfo.vrf, bInfo.vrf)
1✔
1512
                        }
1✔
1513
                } else {
1✔
1514
                        // If config from Netspoc has no interface definitions, it is
1✔
1515
                        // probably of type "managed=routing_only", and Netspoc won't
1✔
1516
                        // change any interface config.
1✔
1517
                        if !aInfo.shut && aInfo.addr != "" && len(bIntf) != 0 {
2✔
1518
                                errlog.Warning(
1✔
1519
                                        "Interface '%s' on device is not known by Netspoc", name)
1✔
1520
                        }
1✔
1521
                }
1522
        }
1523
        for _, c := range s.SpocCfg.lookup["interface"][""] {
2✔
1524
                name := strings.Fields(c.parsed)[1]
1✔
1525
                if !aKnown[name] {
2✔
1526
                        return fmt.Errorf(
1✔
1527
                                "Interface '%s' from Netspoc not known on device", name)
1✔
1528
                }
1✔
1529
        }
1530
        return nil
1✔
1531
}
1532

1533
func (s *State) alignVRFs() {
1✔
1534
        interfaceVRF := func(c *cmd) string {
2✔
1535
                for _, s := range c.sub {
2✔
1536
                        if _, v, found := strings.Cut(s.orig, "vrf forwarding "); found {
2✔
1537
                                return v
1✔
1538
                        }
1✔
1539
                }
1540
                return ""
1✔
1541
        }
1542
        routeVRF := func(c *cmd) string {
2✔
1543
                tokens := strings.Fields(c.parsed)
1✔
1544
                if tokens[2] == "vrf" {
2✔
1545
                        return tokens[3]
1✔
1546
                }
1✔
1547
                return ""
1✔
1548
        }
1549
        pairs := []struct {
1✔
1550
                name string
1✔
1551
                get  func(c *cmd) string
1✔
1552
        }{
1✔
1553
                {"interface", interfaceVRF},
1✔
1554
                {"ip route", routeVRF},
1✔
1555
        }
1✔
1556
        // Find VRFs used in Netspoc configuration.
1✔
1557
        bVRF := make(map[string]bool)
1✔
1558
        for _, p := range pairs {
2✔
1559
                for _, c := range s.SpocCfg.lookup[p.name][""] {
2✔
1560
                        bVRF[p.get(c)] = true
1✔
1561
                }
1✔
1562
        }
1563
        // Leave device config unchanged if approve is run with empty
1564
        // Netspoc config for testing.
1565
        if len(bVRF) == 0 {
2✔
1566
                return
1✔
1567
        }
1✔
1568
        // Remove VRFs from device configuration that are not handled by Netspoc.
1569
        removed := make(map[string]bool)
1✔
1570
        for _, p := range pairs {
2✔
1571
                if l := s.DeviceCfg.lookup[p.name][""]; l != nil {
2✔
1572
                        j := 0
1✔
1573
                        for _, c := range l {
2✔
1574
                                vrf := p.get(c)
1✔
1575
                                if bVRF[vrf] {
2✔
1576
                                        l[j] = c
1✔
1577
                                        j++
1✔
1578
                                } else {
2✔
1579
                                        // If this unmanaged interface references some ACL, that
1✔
1580
                                        // was previously generated by Netspoc, this ACL must
1✔
1581
                                        // not accidently be deleted.
1✔
1582
                                        s.markNeeded(c.sub)
1✔
1583
                                        removed[vrf] = true
1✔
1584
                                }
1✔
1585
                        }
1586
                        l = l[:j]
1✔
1587
                        if len(l) != 0 {
2✔
1588
                                s.DeviceCfg.lookup[p.name][""] = l
1✔
1589
                        } else {
2✔
1590
                                delete(s.DeviceCfg.lookup[p.name], "")
1✔
1591
                        }
1✔
1592
                }
1593
        }
1594
        for _, vrf := range slices.Sorted(maps.Keys(removed)) {
2✔
1595
                if vrf == "" {
2✔
1596
                        vrf = "<global>"
1✔
1597
                }
1✔
1598
                errlog.Info("Leaving VRF %s untouched", vrf)
1✔
1599
        }
1600
}
1601

1602
// Mark commands and referenced commands as still needed.
1603
func (s *State) markNeeded(l []*cmd) {
1✔
1604
        for _, c := range l {
2✔
1605
                c.needed = true
1✔
1606
                for i, name := range c.ref {
2✔
1607
                        prefix := c.typ.ref[i]
1✔
1608
                        s.markNeeded(s.DeviceCfg.lookup[prefix][name])
1✔
1609
                }
1✔
1610
                s.markNeeded(c.sub)
1✔
1611
        }
1612
}
1613

1614
// 'crypto map gdoi' is currently not supported by Netspoc.
1615
// That commands must be left unchanged on device.
1616
func (s *State) ignoreCryptoGDOI() {
1✔
1617
        rm := make(map[string]bool)
1✔
1618
        for _, c := range s.DeviceCfg.lookup["interface"][""] {
2✔
1619
                l := c.sub
1✔
1620
                j := 0
1✔
1621
                for _, sc := range l {
2✔
1622
                        if sc.parsed == "crypto map $REF" {
2✔
1623
                                name := sc.ref[0]
1✔
1624
                                l2 := s.DeviceCfg.lookup["crypto map"][name]
1✔
1625
                                if l2[0].parsed == "crypto map $NAME $SEQ gdoi" {
2✔
1626
                                        rm[name] = true
1✔
1627
                                        continue
1✔
1628
                                }
1629
                        }
1630
                        l[j] = sc
1✔
1631
                        j++
1✔
1632
                }
1633
                c.sub = l[:j]
1✔
1634
        }
1635
        for name := range rm {
2✔
1636
                delete(s.DeviceCfg.lookup["crypto map"], name)
1✔
1637
        }
1✔
1638
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc