mirror of
https://github.com/containers/podman.git
synced 2026-07-09 14:54:59 -04:00
podman: Add support for blackhole routes
Add support for blackhole, unreachable, and prohibit route types in podman networks. These route types allow silently discarding packets (blackhole), rejecting with destination unreachable (unreachable), or rejecting with administratively prohibited (prohibit). Note: Blackhole routes require netavark >= 2.0.0. Regular unicast routes remain backward compatible with all netavark versions. Signed-off-by: Povilas Kanapickas <povilas@radix.lt> <MH: Rebased atop latest main & fixed cherry-pick conflicts> Signed-off-by: Matt Heon <matthew.heon@pm.me>
This commit is contained in:
committed by
Matt Heon
parent
8cc0a6774e
commit
daaf8b62ba
@@ -73,7 +73,7 @@ func networkCreateFlags(cmd *cobra.Command) {
|
||||
_ = cmd.RegisterFlagCompletionFunc(subnetFlagName, completion.AutocompleteNone)
|
||||
|
||||
routeFlagName := "route"
|
||||
flags.StringArrayVar(&networkCreateOptions.Routes, routeFlagName, nil, "static routes")
|
||||
flags.StringArrayVar(&networkCreateOptions.Routes, routeFlagName, nil, "Static routes for this network. Format: <destination>,<gateway>[,<metric>] or <destination>,<type>[,<metric>] where type is blackhole, unreachable, or prohibit")
|
||||
_ = cmd.RegisterFlagCompletionFunc(routeFlagName, completion.AutocompleteNone)
|
||||
|
||||
interfaceFlagName := "interface-name"
|
||||
@@ -192,41 +192,57 @@ func networkCreate(cmd *cobra.Command, args []string) error {
|
||||
|
||||
func parseRoute(routeStr string) (*types.Route, error) {
|
||||
s := strings.Split(routeStr, ",")
|
||||
var metric *uint32
|
||||
|
||||
if len(s) == 2 || len(s) == 3 {
|
||||
dstStr := s[0]
|
||||
gwStr := s[1]
|
||||
if len(s) < 2 || len(s) > 3 {
|
||||
return nil, fmt.Errorf("invalid route: %s\nFormat: --route <destination>,<gateway>[,<metric>] or --route <destination>,<type>[,<metric>] where type is blackhole, unreachable, or prohibit", routeStr)
|
||||
}
|
||||
|
||||
destination, err := types.ParseCIDR(dstStr)
|
||||
gateway := net.ParseIP(gwStr)
|
||||
destination, err := types.ParseCIDR(s[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid route destination %s: %w", s[0], err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid route destination %s", dstStr)
|
||||
}
|
||||
route := &types.Route{
|
||||
Destination: destination,
|
||||
}
|
||||
|
||||
if gateway == nil {
|
||||
return nil, fmt.Errorf("invalid route gateway %s", gwStr)
|
||||
}
|
||||
|
||||
if len(s) == 3 {
|
||||
// Check if second field is a route type (blackhole, unreachable, prohibit)
|
||||
secondField := strings.ToLower(s[1])
|
||||
switch types.RouteType(secondField) {
|
||||
case types.RouteTypeBlackhole, types.RouteTypeUnreachable, types.RouteTypeProhibit:
|
||||
route.RouteType = types.RouteType(secondField)
|
||||
// Parse optional metric from position 2
|
||||
if len(s) >= 3 {
|
||||
mtr, err := strconv.ParseUint(s[2], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid route metric %s", s[2])
|
||||
}
|
||||
x := uint32(mtr)
|
||||
metric = &x
|
||||
route.Metric = &x
|
||||
}
|
||||
|
||||
r := types.Route{
|
||||
Destination: destination,
|
||||
Gateway: gateway,
|
||||
Metric: metric,
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
return route, nil
|
||||
case types.RouteTypeUnicast:
|
||||
return nil, fmt.Errorf("unicast route requires a gateway, format: --route <destination>,<gateway>[,<metric>]")
|
||||
}
|
||||
return nil, fmt.Errorf("invalid route: %s\nFormat: --route <destination in CIDR>,<gateway>,<metric (optional)>", routeStr)
|
||||
|
||||
// Regular unicast route with gateway
|
||||
gateway := net.ParseIP(s[1])
|
||||
if gateway == nil {
|
||||
return nil, fmt.Errorf("invalid route gateway %s", s[1])
|
||||
}
|
||||
route.Gateway = gateway
|
||||
route.RouteType = types.RouteTypeUnicast // Default
|
||||
|
||||
// Parse optional metric
|
||||
if len(s) >= 3 {
|
||||
mtr, err := strconv.ParseUint(s[2], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid route metric %s", s[2])
|
||||
}
|
||||
x := uint32(mtr)
|
||||
route.Metric = &x
|
||||
}
|
||||
return route, nil
|
||||
}
|
||||
|
||||
func parseRange(iprange string) (*types.LeaseRange, error) {
|
||||
|
||||
222
cmd/podman/networks/create_test.go
Normal file
222
cmd/podman/networks/create_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.podman.io/common/libnetwork/types"
|
||||
)
|
||||
|
||||
func TestParseRoute(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
routeStr string
|
||||
want *types.Route
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
// Valid unicast routes (2 fields)
|
||||
{
|
||||
name: "unicast route with destination and gateway",
|
||||
routeStr: "10.21.0.0/24,10.19.12.250",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "10.21.0.0/24"),
|
||||
Gateway: net.ParseIP("10.19.12.250"),
|
||||
RouteType: types.RouteTypeUnicast,
|
||||
},
|
||||
},
|
||||
// Valid unicast routes (3 fields with metric)
|
||||
{
|
||||
name: "unicast route with destination, gateway and metric",
|
||||
routeStr: "10.21.0.0/24,10.19.12.250,100",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "10.21.0.0/24"),
|
||||
Gateway: net.ParseIP("10.19.12.250"),
|
||||
Metric: uint32Ptr(100),
|
||||
RouteType: types.RouteTypeUnicast,
|
||||
},
|
||||
},
|
||||
// Valid blackhole routes (2 fields)
|
||||
{
|
||||
name: "blackhole route with destination only",
|
||||
routeStr: "10.21.0.0/24,blackhole",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "10.21.0.0/24"),
|
||||
RouteType: types.RouteTypeBlackhole,
|
||||
},
|
||||
},
|
||||
// Valid blackhole routes (3 fields with metric)
|
||||
{
|
||||
name: "blackhole route with destination and metric",
|
||||
routeStr: "10.21.0.0/24,blackhole,200",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "10.21.0.0/24"),
|
||||
Metric: uint32Ptr(200),
|
||||
RouteType: types.RouteTypeBlackhole,
|
||||
},
|
||||
},
|
||||
// Valid unreachable routes
|
||||
{
|
||||
name: "unreachable route with destination only",
|
||||
routeStr: "192.168.100.0/24,unreachable",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "192.168.100.0/24"),
|
||||
RouteType: types.RouteTypeUnreachable,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unreachable route with destination and metric",
|
||||
routeStr: "192.168.100.0/24,unreachable,150",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "192.168.100.0/24"),
|
||||
Metric: uint32Ptr(150),
|
||||
RouteType: types.RouteTypeUnreachable,
|
||||
},
|
||||
},
|
||||
// Valid prohibit routes
|
||||
{
|
||||
name: "prohibit route with destination only",
|
||||
routeStr: "172.16.0.0/16,prohibit",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "172.16.0.0/16"),
|
||||
RouteType: types.RouteTypeProhibit,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prohibit route with destination and metric",
|
||||
routeStr: "172.16.0.0/16,prohibit,50",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "172.16.0.0/16"),
|
||||
Metric: uint32Ptr(50),
|
||||
RouteType: types.RouteTypeProhibit,
|
||||
},
|
||||
},
|
||||
// Case insensitivity for route types
|
||||
{
|
||||
name: "blackhole route type uppercase",
|
||||
routeStr: "10.21.0.0/24,BLACKHOLE",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "10.21.0.0/24"),
|
||||
RouteType: types.RouteTypeBlackhole,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unreachable route type mixed case",
|
||||
routeStr: "10.21.0.0/24,Unreachable",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "10.21.0.0/24"),
|
||||
RouteType: types.RouteTypeUnreachable,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prohibit route type mixed case",
|
||||
routeStr: "10.21.0.0/24,ProHiBit",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "10.21.0.0/24"),
|
||||
RouteType: types.RouteTypeProhibit,
|
||||
},
|
||||
},
|
||||
// IPv6 routes
|
||||
{
|
||||
name: "unicast IPv6 route",
|
||||
routeStr: "fd00:1::/64,fd00::1",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "fd00:1::/64"),
|
||||
Gateway: net.ParseIP("fd00::1"),
|
||||
RouteType: types.RouteTypeUnicast,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "blackhole IPv6 route",
|
||||
routeStr: "fd00:2::/64,blackhole",
|
||||
want: &types.Route{
|
||||
Destination: mustParseCIDR(t, "fd00:2::/64"),
|
||||
RouteType: types.RouteTypeBlackhole,
|
||||
},
|
||||
},
|
||||
// Invalid routes - too few fields
|
||||
{
|
||||
name: "route with only destination",
|
||||
routeStr: "10.21.0.0/24",
|
||||
wantErr: true,
|
||||
errContains: "invalid route",
|
||||
},
|
||||
// Invalid routes - too many fields
|
||||
{
|
||||
name: "route with too many fields",
|
||||
routeStr: "10.21.0.0/24,10.19.12.250,100,extra",
|
||||
wantErr: true,
|
||||
errContains: "invalid route",
|
||||
},
|
||||
// Invalid routes - unicast without gateway
|
||||
{
|
||||
name: "unicast route type without gateway",
|
||||
routeStr: "10.21.0.0/24,unicast",
|
||||
wantErr: true,
|
||||
errContains: "unicast route requires a gateway",
|
||||
},
|
||||
// Invalid routes - bad destination
|
||||
{
|
||||
name: "invalid destination CIDR",
|
||||
routeStr: "invalid-cidr,10.19.12.250",
|
||||
wantErr: true,
|
||||
errContains: "invalid route destination",
|
||||
},
|
||||
// Invalid routes - bad gateway
|
||||
{
|
||||
name: "invalid gateway IP",
|
||||
routeStr: "10.21.0.0/24,invalid-gateway",
|
||||
wantErr: true,
|
||||
errContains: "invalid route gateway",
|
||||
},
|
||||
// Invalid routes - bad metric
|
||||
{
|
||||
name: "invalid metric for unicast route",
|
||||
routeStr: "10.21.0.0/24,10.19.12.250,invalid-metric",
|
||||
wantErr: true,
|
||||
errContains: "invalid route metric",
|
||||
},
|
||||
{
|
||||
name: "invalid metric for blackhole route",
|
||||
routeStr: "10.21.0.0/24,blackhole,invalid-metric",
|
||||
wantErr: true,
|
||||
errContains: "invalid route metric",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseRoute(tt.routeStr)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
if tt.errContains != "" {
|
||||
assert.Contains(t, err.Error(), tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want.Destination.String(), got.Destination.String())
|
||||
assert.Equal(t, tt.want.Gateway, got.Gateway)
|
||||
assert.Equal(t, tt.want.RouteType, got.RouteType)
|
||||
if tt.want.Metric != nil {
|
||||
require.NotNil(t, got.Metric)
|
||||
assert.Equal(t, *tt.want.Metric, *got.Metric)
|
||||
} else {
|
||||
assert.Nil(t, got.Metric)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseCIDR(t *testing.T, cidr string) types.IPNet {
|
||||
t.Helper()
|
||||
ipnet, err := types.ParseCIDR(cidr)
|
||||
require.NoError(t, err)
|
||||
return ipnet
|
||||
}
|
||||
|
||||
func uint32Ptr(v uint32) *uint32 {
|
||||
return &v
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"go.podman.io/common/libnetwork/types"
|
||||
"go.podman.io/podman/v6/pkg/domain/entities"
|
||||
. "go.podman.io/podman/v6/test/utils"
|
||||
"go.podman.io/storage/pkg/stringid"
|
||||
)
|
||||
@@ -19,6 +21,10 @@ func removeNetworkDevice(name string) {
|
||||
session.WaitWithDefaultTimeout()
|
||||
}
|
||||
|
||||
func uintPtr(u uint32) *uint32 {
|
||||
return &u
|
||||
}
|
||||
|
||||
var _ = Describe("Podman network create", func() {
|
||||
It("podman network create with name and subnet", func() {
|
||||
netName := "subnet-" + stringid.GenerateRandomID()
|
||||
@@ -660,4 +666,51 @@ var _ = Describe("Podman network create", func() {
|
||||
// All we care about is the ip is from the range which allows for both.
|
||||
Expect(containerIP.String()).To(Or(Equal("10.11.16.11"), Equal("10.11.16.12")), "ip address must be in --ip-range")
|
||||
})
|
||||
|
||||
DescribeTable("podman network create with special route types",
|
||||
func(subnet string, route string, expectedDest string, expectedRouteType types.RouteType, expectedMetric *uint32) {
|
||||
SkipIfNetavarkVersionLessThan("2.0.0")
|
||||
netName := "subnet-" + stringid.GenerateRandomID()
|
||||
nc := podmanTest.Podman([]string{
|
||||
"network",
|
||||
"create",
|
||||
"--subnet",
|
||||
subnet,
|
||||
"--route",
|
||||
route,
|
||||
netName,
|
||||
})
|
||||
nc.WaitWithDefaultTimeout()
|
||||
defer podmanTest.removeNetwork(netName)
|
||||
Expect(nc).Should(ExitCleanly())
|
||||
|
||||
inspect := podmanTest.Podman([]string{"network", "inspect", netName})
|
||||
inspect.WaitWithDefaultTimeout()
|
||||
Expect(inspect).Should(ExitCleanly())
|
||||
|
||||
var results []entities.NetworkInspectReport
|
||||
err := json.Unmarshal([]byte(inspect.OutputToString()), &results)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(results).To(HaveLen(1))
|
||||
result := results[0]
|
||||
Expect(result).To(HaveField("Name", netName))
|
||||
Expect(result.Subnets).To(HaveLen(1))
|
||||
Expect(result.Subnets[0].Subnet.String()).To(Equal(subnet))
|
||||
Expect(result.Routes).To(HaveLen(1))
|
||||
Expect(result.Routes[0].Destination.String()).To(Equal(expectedDest))
|
||||
Expect(result.Routes[0].Gateway).To(BeNil())
|
||||
Expect(result.Routes[0].RouteType).To(Equal(expectedRouteType))
|
||||
if expectedMetric != nil {
|
||||
Expect(*result.Routes[0].Metric).To(Equal(*expectedMetric))
|
||||
} else {
|
||||
Expect(result.Routes[0].Metric).To(BeNil())
|
||||
}
|
||||
|
||||
defer removeNetworkDevice(result.NetworkInterface)
|
||||
},
|
||||
Entry("blackhole route", "10.19.20.0/24", "10.21.10.0/24,blackhole", "10.21.10.0/24", types.RouteTypeBlackhole, nil),
|
||||
Entry("unreachable route", "10.19.21.0/24", "10.21.11.0/24,unreachable", "10.21.11.0/24", types.RouteTypeUnreachable, nil),
|
||||
Entry("prohibit route", "10.19.22.0/24", "10.21.12.0/24,prohibit", "10.21.12.0/24", types.RouteTypeProhibit, nil),
|
||||
Entry("blackhole route with metric", "10.19.23.0/24", "10.21.13.0/24,blackhole,250", "10.21.13.0/24", types.RouteTypeBlackhole, uintPtr(250)),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user