Skip to content

Commit

Permalink
Added support for dual stack IPv4/v6 objects
Browse files Browse the repository at this point in the history
  • Loading branch information
hknutzen committed Dec 16, 2024
1 parent 8b633e1 commit d94d0df
Show file tree
Hide file tree
Showing 56 changed files with 3,587 additions and 1,177 deletions.
42 changes: 42 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
{{$NEXT}}

- Added support for dual stack objects, having both, IPv4 and IPv6 addresses.
This simplifies the modeling of a dual stack toplology.
It is no longer neccessary to model a separate IPv4 and IPv6 topology.
Rules between dual stack objects will generate ACls for IPv4 and IPv6.
Rules between dual stack object and pure IPv4 object will silently ignore
IPv6 address and generate only ACL for IPv4.
New attributes have been introduced to define dual stack objects:
- 'ip6' at network, host, interface and aggregate.
- 'range6' at host.
- 'unnumbered6' at network and interface.
- 'negotiated6' at interface.
Other changes resulting from use of dual stack objects:
- If dual stack objects are used as border or inclusive border of an area,
this defines two areas with identical name:
one in IPv4 topology and one in IPv6 topology.
- If dual stack objects are used as interfaces of an pathrestriction,
this also defines two pathrestrictions in IPv4 and IPv6 topology.
If the second pathrestriction has only one interface or only interfaces
outside of a loop, it is silently ignored.
- New attributes 'ipv4_only' and 'ipv6_only' may be used at service or area.
This will enable only IPv4 or IPv6 part of dual stack objects.
- The following attributes are applied only to IPv4 part
if used in dual stack objects:
'nat', 'bind_nat', 'subnet_of, 'hub', 'spoke'.
- Command 'export-netspoc' has been changed to show IPv6 address of
dual stack objects in attribute 'ip6'.
- Added new attribute 'auto_ipv6_hosts' to automatically generate
dual stack hosts from pure IPv4 hosts.
It will generate IPv6 addresses for hosts
by combining its IPv4 adress with the IPv6 address of its network.
These attribute values are provided:
- auto_ipv6_hosts = readable
network: ip6 = 2001:db8:1:1::/64;
hosts: ip = 172.17.1.48;
==> 2001:db8:1:1:172:17:1:48
- auto_ipv6_hosts = binary
network: ip6 = 2001:db8:1:1::/64;
hosts: ip = 172.17.1.48;
==> 2001:db8:1:1::ac11:130
- auto_ipv6_hosts = none
No IPv6 address is generated
This attribute can be used at network, area and host.
- Fixed command 'cut-netspoc':
Full path to management_instance is marked now.

Expand Down
3 changes: 2 additions & 1 deletion go/pkg/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ func (a SimpleAuto) String() string {

type AggAuto struct {
SimpleAuto
Net string
IPV6 bool
Net string
}

func (a AggAuto) String() string {
Expand Down
10 changes: 9 additions & 1 deletion go/pkg/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,14 @@ func (p *parser) simpleAuto(typ string) ast.Element {
func (p *parser) aggAuto(typ string) ast.Element {
a := new(ast.AggAuto)
a.Type = typ
a.IPV6 = p.ipv6
p.next()
if p.check("ip") {
switch p.tok {
case "ip6":
a.IPV6 = true
fallthrough
case "ip":
p.next()
p.expect("=")
a.Net = p.name()
p.expect("&")
Expand Down Expand Up @@ -429,6 +435,7 @@ var specialTokenAttr = map[string]func(*parser){
"admins": (*parser).nextMulti,
"watchers": (*parser).nextMulti,
"range": (*parser).nextIPRange,
"range6": (*parser).nextIPRange,
}

var specialSubTokenAttr = map[string]func(*parser){
Expand All @@ -440,6 +447,7 @@ var specialValueAttr = map[string]func(*parser, func(*parser)) *ast.Value{
"general_permit": (*parser).protocolRef,
"lifetime": (*parser).multiValue,
"range": (*parser).multiValue,
"range6": (*parser).multiValue,
}

func (p *parser) specialAttribute(nextSpecial func(*parser)) *ast.Attribute {
Expand Down
9 changes: 4 additions & 5 deletions go/pkg/pass1/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,26 @@ func (obj *network) address(m natMap) netip.Prefix {

func (obj *subnet) address(m natMap) netip.Prefix {
n := getNatNetwork(obj.network, m)
return natAddress(obj.ipp.Addr(), obj.ipp.Bits(), obj.nat, n, obj.network.ipV6)
return natAddress(obj.ipp.Addr(), obj.ipp.Bits(), obj.nat, n)
}

func (obj *routerIntf) address(m natMap) netip.Prefix {
n := getNatNetwork(obj.network, m)
if obj.ipType == negotiatedIP {
return n.ipp
}
ipV6 := obj.network.ipV6
return natAddress(obj.ip, getHostPrefix(ipV6), obj.nat, n, ipV6)
return natAddress(obj.ip, obj.ip.BitLen(), obj.nat, n)
}

func natAddress(ip netip.Addr, bits int, nat map[string]netip.Addr,
n *network, ipV6 bool) netip.Prefix {
n *network) netip.Prefix {

if n.dynamic {
natTag := n.natTag
if ip, ok := nat[natTag]; ok {

// Single static NAT IP for this interface.
return netip.PrefixFrom(ip, getHostPrefix(ipV6))
return netip.PrefixFrom(ip, ip.BitLen())
} else {
return n.ipp
}
Expand Down
95 changes: 95 additions & 0 deletions go/pkg/pass1/auto-ipv6-hosts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package pass1

import (
"fmt"
"net/netip"
"strings"

"github.com/hknutzen/Netspoc/go/pkg/ast"
"go4.org/netipx"
)

const (
hostsUnset = iota
hostsNone
hostsReadable
hostsBinary
)

func (c *spoc) getAutoIPv6Hosts(a *ast.Attribute, ctx string) string {
v := c.getSingleValue(a, ctx)
switch v {
default:
c.err("Expected 'readable|binary|none' in '%s' of %s", a.Name, ctx)
case "readable", "binary", "none":
}
return v
}

func (c *spoc) addAutoIPv6Hosts() {
for _, n := range c.allNetworks {
attr := n.autoIPv6Hosts
if !n.ipV6 {
if n.combined46 == nil && attr != "" {
c.warn("Ignoring 'auto_ipv6_hosts' at IPv4 only %s", n)
}
continue
}
if attr == "" || attr == "none" {
continue
}
n4 := n.combined46
if n4 == nil {
c.warn("Ignoring 'auto_ipv6_hosts' at IPv6 only %s", n)
continue
}
ipp := n.ipp
if ipp.Bits() > 64 {
c.err("Can't use 'auto_ipv6_hosts' at %s having prefix len > 64", n)
continue
}
for _, h4 := range n4.hosts {
if h4.combined46 == nil {
attr2 := attr
if at := h4.autoIPv6Hosts; at != "" {
if at == "none" {
continue
}
attr2 = at
}
cp := *h4
cp.ipV6 = true
cp.combined46 = h4
h4.combined46 = &cp
if h4.ip.IsValid() {
cp.ip = genIPv6FromIPv4(h4.ip, ipp, attr2)
} else if h4.ipRange.IsValid() {
from := genIPv6FromIPv4(h4.ipRange.From(), ipp, attr2)
to := genIPv6FromIPv4(h4.ipRange.To(), ipp, attr2)
cp.ipRange = netipx.IPRangeFrom(from, to)
}
cp.nat = nil
n.hosts = append(n.hosts, &cp)
}
}
}
}

func genIPv6FromIPv4(ip netip.Addr, ipp netip.Prefix, attr string) netip.Addr {
b16 := ipp.Addr().AsSlice()
switch attr {
case "binary":
b4 := ip.AsSlice()
copy(b16[12:], b4)
case "readable":
s4 := ip.String()
dec := strings.Split(s4, ".")
s := fmt.Sprintf("%04s%04s%04s%04s", dec[0], dec[1], dec[2], dec[3])
for i := 0; i < 8; i++ {
x := s[2*i : 2*i+2]
fmt.Sscanf(x, "%x", &b16[8+i])
}
}
result, _ := netip.AddrFromSlice(b16)
return result
}
8 changes: 4 additions & 4 deletions go/pkg/pass1/check-ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (c *spoc) checkSubnetOf() {
return
}
if !sn.ipp.Contains(n.ipp.Addr()) {
c.err("%s is subnet_of %s but its IP doesn't match that's IP/mask",
c.err("%s is subnet_of %s but its IP doesn't match that's address",
ctx, sn)
}
}
Expand All @@ -45,7 +45,7 @@ func (c *spoc) checkSubnetOf() {
checkNat(nat)
}
}
for _, ar := range c.symTable.area {
for _, ar := range c.ascendingAreas {
if nat := ar.nat; nat != nil {
checkNat(nat)
}
Expand Down Expand Up @@ -214,7 +214,7 @@ func (c *spoc) checkBridgedNetworks(prefix string, l netList) {
n2 := next[0]
next = next[1:]
if n1.ipp != n2.ipp {
c.err("%s and %s must have identical ip/mask", n1, n2)
c.err("%s and %s must have identical address", n1, n2)
}
connected[n2] = true
for _, in := range n2.interfaces {
Expand All @@ -229,7 +229,7 @@ func (c *spoc) checkBridgedNetworks(prefix string, l netList) {
single := true
if l3 := in.layer3Intf; l3 != nil {
if !n1.ipp.Contains(l3.ip) {
c.err("%s's IP doesn't match IP/mask of bridged networks",
c.err("%s's IP doesn't match address of bridged networks",
l3)
}
}
Expand Down
4 changes: 2 additions & 2 deletions go/pkg/pass1/check-service-owner.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (c *spoc) propagateOwners() {
if !o.showAll {
continue
}
var invalid stringList
var invalid netList
for _, n := range c.allNetworks {
if netOwner := n.owner; netOwner != nil {
if netOwner == o {
Expand All @@ -116,7 +116,7 @@ func (c *spoc) propagateOwners() {
continue
}
if !slices.Contains(n.zone.watchingOwners, o) {
invalid.push(n.name)
invalid.push(n)
}
}
if invalid != nil {
Expand Down
3 changes: 3 additions & 0 deletions go/pkg/pass1/collect-routers-and-networks.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,8 @@ func (c *spoc) collectRoutersAndNetworks() {
if !seen[n] {
c.allNetworks.push(n)
}
if n6 := n.combined46; n6 != nil && !seen[n6] {
c.allNetworks.push(n6)
}
}
}
13 changes: 4 additions & 9 deletions go/pkg/pass1/convert-hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,7 @@ func (c *spoc) convertHosts() {
if n.ipType == unnumberedIP || n.ipType == tunnelIP {
continue
}
ipv6 := n.ipV6
var bitstrLen int
if ipv6 {
bitstrLen = 128
} else {
bitstrLen = 32
}
bitstrLen := n.ipp.Addr().BitLen()
subnetAref := make([]map[netip.Addr]*subnet, bitstrLen)

// Converts hosts and ranges to subnets.
Expand All @@ -46,8 +40,7 @@ func (c *spoc) convertHosts() {
name := host.name
id := host.id
if host.ip.IsValid() {
nets = []netip.Prefix{
netip.PrefixFrom(host.ip, getHostPrefix(ipv6))}
nets = []netip.Prefix{netip.PrefixFrom(host.ip, bitstrLen)}
if id != "" {
switch strings.Index(id, "@") {
case 0:
Expand Down Expand Up @@ -89,6 +82,7 @@ func (c *spoc) convertHosts() {
s.name = name
s.network = n
s.ipp = ipp
s.ipV6 = host.ipV6
s.nat = host.nat
s.owner = host.owner
s.id = id
Expand Down Expand Up @@ -195,6 +189,7 @@ func (c *spoc) convertHosts() {
u.name = name
u.network = n
u.ipp = upNet
u.ipV6 = s.ipV6
u.up = s.up
upIP2subnet := subnetAref[upSubnetSize]
if upIP2subnet == nil {
Expand Down
4 changes: 2 additions & 2 deletions go/pkg/pass1/cut-netspoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ func (c *spoc) markAndSubstElements(
isUsed map[string]bool) {

expand := func(el ast.Element) groupObjList {
return c.expandGroup1([]ast.Element{el}, ctx, v6, false, false)
return c.expandGroup1([]ast.Element{el}, ctx, false, false)
}
toAST := func(obj groupObj) ast.Element {
var result ast.Element
Expand Down Expand Up @@ -626,7 +626,7 @@ func (c *spoc) cutNetspoc(
// Mark management_instance of routers
for _, r := range c.managedRouters {
if isUsed[r.name] && r.model.needManagementInstance {
if mr := c.getRouter(r.deviceName, r.ipV6); mr != nil {
if mr := c.symTable.router[r.deviceName]; mr != nil {
for _, intf := range getIntf(mr) {
c.markPath(intf, r.interfaces[0], isUsed)
}
Expand Down
8 changes: 6 additions & 2 deletions go/pkg/pass1/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ func (l intfList) nameList() string {

func (l netList) nameList() string {
var names stringList
for _, intf := range l {
names.push(intf.name)
for _, n := range l {
name := n.name
if n.isCombined46() {
name = cond(n.ipV6, "IPv6", "IPv4") + " " + name
}
names.push(name)
}
return names.nameList()
}
Expand Down
Loading

0 comments on commit d94d0df

Please sign in to comment.