Skip to content

Commit b976224

Browse files
authored
Merge pull request #2090 from davidbockelman/override-ext-address
Allow overriding the external address (NAT) for OVN NICs
2 parents f0f4dc0 + 8f6d9f1 commit b976224

File tree

6 files changed

+155
-0
lines changed

6 files changed

+155
-0
lines changed

doc/api-extensions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2811,3 +2811,9 @@ This API extension provides the ability to configure certificates in preseed ini
28112811
## `custom_volume_sftp`
28122812

28132813
This adds the SFTP API to custom storage volumes.
2814+
2815+
## `network_ovn_external_nic_address`
2816+
2817+
This adds support for configuring a custom external IPv4 or IPv6 address
2818+
for a given instance so long as that address is available through a
2819+
network forward.

doc/config_options.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,13 @@ For file systems (shared directories or custom volumes), this is one of:
847847

848848
```
849849

850+
```{config:option} ipv4.address.external devices-nic_ovn
851+
:managed: "no"
852+
:shortdesc: "Select a specific external address (typically from a network forward)"
853+
:type: "string"
854+
855+
```
856+
850857
```{config:option} ipv4.routes devices-nic_ovn
851858
:managed: "no"
852859
:shortdesc: "Comma-delimited list of IPv4 static routes to route to the NIC"
@@ -868,6 +875,13 @@ For file systems (shared directories or custom volumes), this is one of:
868875

869876
```
870877

878+
```{config:option} ipv6.address.external devices-nic_ovn
879+
:managed: "no"
880+
:shortdesc: "Select a specific external address (typically from a network forward)"
881+
:type: "string"
882+
883+
```
884+
871885
```{config:option} ipv6.routes devices-nic_ovn
872886
:managed: "no"
873887
:shortdesc: "Comma-delimited list of IPv6 static routes to route to the NIC"

internal/server/device/nic_ovn.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,22 @@ func (d *nicOVN) validateConfig(instConf instance.ConfigReader) error {
149149
// shortdesc: An IPv6 address to assign to the instance through DHCP, `none` can be used to disable IP allocation
150150
"ipv6.address",
151151

152+
// gendoc:generate(entity=devices, group=nic_ovn, key=ipv4.address.external)
153+
//
154+
// ---
155+
// type: string
156+
// managed: no
157+
// shortdesc: Select a specific external address (typically from a network forward)
158+
"ipv4.address.external",
159+
160+
// gendoc:generate(entity=devices, group=nic_ovn, key=ipv6.address.external)
161+
//
162+
// ---
163+
// type: string
164+
// managed: no
165+
// shortdesc: Select a specific external address (typically from a network forward)
166+
"ipv6.address.external",
167+
152168
// gendoc:generate(entity=devices, group=nic_ovn, key=ipv4.routes)
153169
//
154170
// ---
@@ -428,6 +444,26 @@ func (d *nicOVN) validateConfig(instConf instance.ConfigReader) error {
428444
return validate.IsNetworkAddressV6(value)
429445
})
430446

447+
// Validate the external address against the list of network forwards.
448+
isNetworkForward := func(value string) error {
449+
return d.state.DB.Cluster.Transaction(context.TODO(), func(ctx context.Context, tx *db.ClusterTx) error {
450+
netID, _, _, err := tx.GetNetworkInAnyState(ctx, networkProjectName, d.config["network"])
451+
if err != nil {
452+
return fmt.Errorf("Failed getting network ID: %w", err)
453+
}
454+
455+
_, _, err = tx.GetNetworkForward(ctx, netID, false, value)
456+
if err != nil {
457+
return fmt.Errorf("External address %q is not a network forward on network %q: %w", value, d.config["network"], err)
458+
}
459+
460+
return nil
461+
})
462+
}
463+
464+
rules["ipv4.address.external"] = validate.Optional(validate.And(validate.IsNetworkAddressV4, isNetworkForward))
465+
rules["ipv6.address.external"] = validate.Optional(validate.And(validate.IsNetworkAddressV6, isNetworkForward))
466+
431467
// Now run normal validation.
432468
err = d.config.Validate(rules)
433469
if err != nil {

internal/server/metadata/configuration.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,14 @@
958958
"type": "string"
959959
}
960960
},
961+
{
962+
"ipv4.address.external": {
963+
"longdesc": "",
964+
"managed": "no",
965+
"shortdesc": "Select a specific external address (typically from a network forward)",
966+
"type": "string"
967+
}
968+
},
961969
{
962970
"ipv4.routes": {
963971
"longdesc": "",
@@ -982,6 +990,14 @@
982990
"type": "string"
983991
}
984992
},
993+
{
994+
"ipv6.address.external": {
995+
"longdesc": "",
996+
"managed": "no",
997+
"shortdesc": "Select a specific external address (typically from a network forward)",
998+
"type": "string"
999+
}
1000+
},
9851001
{
9861002
"ipv6.routes": {
9871003
"longdesc": "",

internal/server/network/driver_ovn.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4387,6 +4387,67 @@ func (n *ovn) InstanceDevicePortStart(opts *OVNInstanceNICSetupOpts, securityACL
43874387
checkAndStoreIP(net.ParseIP(staticIP))
43884388
}
43894389

4390+
// Apply device specific external address if any.
4391+
for _, keyPrefix := range []string{"ipv4", "ipv6"} {
4392+
// Check if the address is present.
4393+
value := opts.DeviceConfig[fmt.Sprintf("%s.address.external", keyPrefix)]
4394+
if value == "" {
4395+
continue
4396+
}
4397+
4398+
// Check if the family is configured.
4399+
if keyPrefix == "ipv4" && ipv4 == "" {
4400+
continue
4401+
}
4402+
4403+
if keyPrefix == "ipv6" && ipv6 == "" {
4404+
continue
4405+
}
4406+
4407+
// Parse the internal address.
4408+
var intNet *net.IPNet
4409+
if keyPrefix == "ipv4" {
4410+
_, intNet, err = net.ParseCIDR(fmt.Sprintf("%s/32", ipv4))
4411+
if err != nil {
4412+
return "", nil, fmt.Errorf("Invalid internal address %q: %w", ipv4, err)
4413+
}
4414+
} else {
4415+
_, intNet, err = net.ParseCIDR(fmt.Sprintf("%s/128", ipv6))
4416+
if err != nil {
4417+
return "", nil, fmt.Errorf("Invalid internal address %q: %w", ipv6, err)
4418+
}
4419+
}
4420+
4421+
// Parse the external address.
4422+
extIP := net.ParseIP(value)
4423+
if extIP == nil {
4424+
return "", nil, fmt.Errorf("Invalid external address %q", value)
4425+
}
4426+
4427+
if err := n.ovnnb.CreateLogicalRouterNAT(
4428+
context.TODO(),
4429+
n.getRouterName(),
4430+
"snat",
4431+
intNet,
4432+
extIP,
4433+
nil,
4434+
false,
4435+
true,
4436+
); err != nil {
4437+
return "", nil, fmt.Errorf("Failed to add SNAT %q: %w", value, err)
4438+
}
4439+
4440+
reverter.Add(func() {
4441+
_ = n.ovnnb.DeleteLogicalRouterNAT(
4442+
context.TODO(),
4443+
n.getRouterName(),
4444+
"snat",
4445+
false,
4446+
extIP,
4447+
)
4448+
})
4449+
}
4450+
43904451
// Get dynamic IPs for switch port if any IPs not assigned statically.
43914452
if (ipv4 != "none" && dnsIPv4 == nil) || (ipv6 != "none" && dnsIPv6 == nil) {
43924453
var dynamicIPs []net.IP
@@ -4916,6 +4977,27 @@ func (n *ovn) InstanceDevicePortStop(ovsExternalOVNPort networkOVN.OVNSwitchPort
49164977
}
49174978
}
49184979

4980+
// Tear down per‑NIC egress SNAT rules (ipv4/ipv6.address.external)
4981+
for _, keyPrefix := range []string{"ipv4", "ipv6"} {
4982+
// Check if the address is present.
4983+
value := opts.DeviceConfig[fmt.Sprintf("%s.address.external", keyPrefix)]
4984+
if value == "" {
4985+
continue
4986+
}
4987+
4988+
// Validate the address.
4989+
extIP := net.ParseIP(value)
4990+
if extIP == nil {
4991+
return fmt.Errorf("Invalid external address %q", value)
4992+
}
4993+
4994+
// Remove the SNAT entry.
4995+
err := n.ovnnb.DeleteLogicalRouterNAT(context.TODO(), n.getRouterName(), "snat", false, extIP)
4996+
if err != nil && !errors.Is(err, networkOVN.ErrNotFound) {
4997+
return err
4998+
}
4999+
}
5000+
49195001
return nil
49205002
}
49215003

internal/version/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ var APIExtensions = []string{
484484
"instance_publish_split",
485485
"init_preseed_certificates",
486486
"custom_volume_sftp",
487+
"network_ovn_external_nic_address",
487488
}
488489

489490
// APIExtensionsCount returns the number of available API extensions.

0 commit comments

Comments
 (0)