diff --git a/cmd/podman/networks/create.go b/cmd/podman/networks/create.go index 84fb48a7f4..8467c83b2e 100644 --- a/cmd/podman/networks/create.go +++ b/cmd/podman/networks/create.go @@ -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: ,[,] or ,[,] 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 ,[,] or --route ,[,] 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 ,[,]") } - return nil, fmt.Errorf("invalid route: %s\nFormat: --route ,,", 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) { diff --git a/cmd/podman/networks/create_test.go b/cmd/podman/networks/create_test.go new file mode 100644 index 0000000000..11fce69eeb --- /dev/null +++ b/cmd/podman/networks/create_test.go @@ -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 +} diff --git a/test/e2e/network_create_test.go b/test/e2e/network_create_test.go index 0872244836..25aa684284 100644 --- a/test/e2e/network_create_test.go +++ b/test/e2e/network_create_test.go @@ -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)), + ) })