diff --git a/api/docs/apis-network.openshift.io/v1.HostSubnet.adoc b/api/docs/apis-network.openshift.io/v1.HostSubnet.adoc index 6d16f2adeda8..961ccd627e78 100644 --- a/api/docs/apis-network.openshift.io/v1.HostSubnet.adoc +++ b/api/docs/apis-network.openshift.io/v1.HostSubnet.adoc @@ -19,7 +19,9 @@ Expand or mouse-over a field for more information about it. ++++
apiVersion: -host: +egressIPs: +
- [string]: +host:hostIP:kind:metadata: diff --git a/api/docs/apis-network.openshift.io/v1.NetNamespace.adoc b/api/docs/apis-network.openshift.io/v1.NetNamespace.adoc index 6d24cadcf76c..1527aa1950f4 100644 --- a/api/docs/apis-network.openshift.io/v1.NetNamespace.adoc +++ b/api/docs/apis-network.openshift.io/v1.NetNamespace.adoc @@ -19,7 +19,9 @@ Expand or mouse-over a field for more information about it. ++++
apiVersion: -kind: +egressIPs: +
- [string]: +kind:metadata:
annotations:
[string]: diff --git a/api/docs/oapi/v1.HostSubnet.adoc b/api/docs/oapi/v1.HostSubnet.adoc index fb22ee20548b..02c3fa118d00 100644 --- a/api/docs/oapi/v1.HostSubnet.adoc +++ b/api/docs/oapi/v1.HostSubnet.adoc @@ -19,7 +19,9 @@ Expand or mouse-over a field for more information about it. ++++apiVersion: -host: +egressIPs: +
- [string]: +host:hostIP:kind:metadata: diff --git a/api/docs/oapi/v1.NetNamespace.adoc b/api/docs/oapi/v1.NetNamespace.adoc index 4f7c3dda6795..de38dee1152c 100644 --- a/api/docs/oapi/v1.NetNamespace.adoc +++ b/api/docs/oapi/v1.NetNamespace.adoc @@ -19,7 +19,9 @@ Expand or mouse-over a field for more information about it. ++++
apiVersion: -kind: +egressIPs: +
- [string]: +kind:metadata:
annotations:
[string]: diff --git a/api/protobuf-spec/github_com_openshift_origin_pkg_network_apis_network_v1.proto b/api/protobuf-spec/github_com_openshift_origin_pkg_network_apis_network_v1.proto index ad7917a65290..a14c8f0e702e 100644 --- a/api/protobuf-spec/github_com_openshift_origin_pkg_network_apis_network_v1.proto +++ b/api/protobuf-spec/github_com_openshift_origin_pkg_network_apis_network_v1.proto @@ -114,6 +114,9 @@ message HostSubnet { // Subnet is the CIDR range of the overlay network assigned to the node for its pods optional string subnet = 4; + + // EgressIPs is the list of automatic egress IP addresses currently hosted by this node + repeated string egressIPs = 5; } // HostSubnetList is a collection of HostSubnets @@ -137,6 +140,10 @@ message NetNamespace { // NetID is the network identifier of the network namespace assigned to each overlay network packet. This can be manipulated with the "oc adm pod-network" commands. optional uint32 netid = 3; + + // EgressIPs is a list of reserved IPs that will be used as the source for external traffic coming from pods in this namespace. (If empty, external traffic will be masqueraded to Node IPs.) + // +optional + repeated string egressIPs = 4; } // NetNamespaceList is a collection of NetNamespaces diff --git a/api/swagger-spec/oapi-v1.json b/api/swagger-spec/oapi-v1.json index 7bb4fdcddec0..3efce6d55c48 100644 --- a/api/swagger-spec/oapi-v1.json +++ b/api/swagger-spec/oapi-v1.json @@ -25756,7 +25756,8 @@ "required": [ "host", "hostIP", - "subnet" + "subnet", + "egressIPs" ], "properties": { "kind": { @@ -25782,6 +25783,13 @@ "subnet": { "type": "string", "description": "Subnet is the CIDR range of the overlay network assigned to the node for its pods" + }, + "egressIPs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "EgressIPs is the list of automatic egress IP addresses currently hosted by this node" } } }, @@ -26908,7 +26916,8 @@ "description": "NetNamespace describes a single isolated network. When using the redhat/openshift-ovs-multitenant plugin, every Namespace will have a corresponding NetNamespace object with the same name. (When using redhat/openshift-ovs-subnet, NetNamespaces are not used.)", "required": [ "netname", - "netid" + "netid", + "egressIPs" ], "properties": { "kind": { @@ -26930,6 +26939,13 @@ "netid": { "type": "integer", "description": "NetID is the network identifier of the network namespace assigned to each overlay network packet. This can be manipulated with the \"oc adm pod-network\" commands." + }, + "egressIPs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "EgressIPs is a list of reserved IPs that will be used as the source for external traffic coming from pods in this namespace. (If empty, external traffic will be masqueraded to Node IPs.)" } } }, diff --git a/api/swagger-spec/openshift-openapi-spec.json b/api/swagger-spec/openshift-openapi-spec.json index 6ffb31970d13..a10c2b19e134 100644 --- a/api/swagger-spec/openshift-openapi-spec.json +++ b/api/swagger-spec/openshift-openapi-spec.json @@ -90978,13 +90978,21 @@ "required": [ "host", "hostIP", - "subnet" + "subnet", + "egressIPs" ], "properties": { "apiVersion": { "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", "type": "string" }, + "egressIPs": { + "description": "EgressIPs is the list of automatic egress IP addresses currently hosted by this node", + "type": "array", + "items": { + "type": "string" + } + }, "host": { "description": "Host is the name of the node. (This is the same as the object's name, but both fields must be set.)", "type": "string" @@ -91069,6 +91077,13 @@ "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", "type": "string" }, + "egressIPs": { + "description": "EgressIPs is a list of reserved IPs that will be used as the source for external traffic coming from pods in this namespace. (If empty, external traffic will be masqueraded to Node IPs.)", + "type": "array", + "items": { + "type": "string" + } + }, "kind": { "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", "type": "string" diff --git a/pkg/network/apis/network/types.go b/pkg/network/apis/network/types.go index a243dfb0974d..c496ba5b3eac 100644 --- a/pkg/network/apis/network/types.go +++ b/pkg/network/apis/network/types.go @@ -50,6 +50,8 @@ type HostSubnet struct { Host string HostIP string Subnet string + + EgressIPs []string } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -65,13 +67,15 @@ type HostSubnetList struct { // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// NetNamespace holds the network id against its name +// NetNamespace holds information about the SDN configuration of a Namespace type NetNamespace struct { metav1.TypeMeta metav1.ObjectMeta NetName string NetID uint32 + + EgressIPs []string } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/network/apis/network/v1/generated.pb.go b/pkg/network/apis/network/v1/generated.pb.go index 27c6d3da1d3d..83c9e785e0de 100644 --- a/pkg/network/apis/network/v1/generated.pb.go +++ b/pkg/network/apis/network/v1/generated.pb.go @@ -415,6 +415,21 @@ func (m *HostSubnet) MarshalTo(dAtA []byte) (int, error) { i++ i = encodeVarintGenerated(dAtA, i, uint64(len(m.Subnet))) i += copy(dAtA[i:], m.Subnet) + if len(m.EgressIPs) > 0 { + for _, s := range m.EgressIPs { + dAtA[i] = 0x2a + i++ + l = len(s) + for l >= 1<<7 { + dAtA[i] = uint8(uint64(l)&0x7f | 0x80) + l >>= 7 + i++ + } + dAtA[i] = uint8(l) + i++ + i += copy(dAtA[i:], s) + } + } return i, nil } @@ -486,6 +501,21 @@ func (m *NetNamespace) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0x18 i++ i = encodeVarintGenerated(dAtA, i, uint64(m.NetID)) + if len(m.EgressIPs) > 0 { + for _, s := range m.EgressIPs { + dAtA[i] = 0x22 + i++ + l = len(s) + for l >= 1<<7 { + dAtA[i] = uint8(uint64(l)&0x7f | 0x80) + l >>= 7 + i++ + } + dAtA[i] = uint8(l) + i++ + i += copy(dAtA[i:], s) + } + } return i, nil } @@ -665,6 +695,12 @@ func (m *HostSubnet) Size() (n int) { n += 1 + l + sovGenerated(uint64(l)) l = len(m.Subnet) n += 1 + l + sovGenerated(uint64(l)) + if len(m.EgressIPs) > 0 { + for _, s := range m.EgressIPs { + l = len(s) + n += 1 + l + sovGenerated(uint64(l)) + } + } return n } @@ -690,6 +726,12 @@ func (m *NetNamespace) Size() (n int) { l = len(m.NetName) n += 1 + l + sovGenerated(uint64(l)) n += 1 + sovGenerated(uint64(m.NetID)) + if len(m.EgressIPs) > 0 { + for _, s := range m.EgressIPs { + l = len(s) + n += 1 + l + sovGenerated(uint64(l)) + } + } return n } @@ -820,6 +862,7 @@ func (this *HostSubnet) String() string { `Host:` + fmt.Sprintf("%v", this.Host) + `,`, `HostIP:` + fmt.Sprintf("%v", this.HostIP) + `,`, `Subnet:` + fmt.Sprintf("%v", this.Subnet) + `,`, + `EgressIPs:` + fmt.Sprintf("%v", this.EgressIPs) + `,`, `}`, }, "") return s @@ -843,6 +886,7 @@ func (this *NetNamespace) String() string { `ObjectMeta:` + strings.Replace(strings.Replace(this.ObjectMeta.String(), "ObjectMeta", "k8s_io_apimachinery_pkg_apis_meta_v1.ObjectMeta", 1), `&`, ``, 1) + `,`, `NetName:` + fmt.Sprintf("%v", this.NetName) + `,`, `NetID:` + fmt.Sprintf("%v", this.NetID) + `,`, + `EgressIPs:` + fmt.Sprintf("%v", this.EgressIPs) + `,`, `}`, }, "") return s @@ -1957,6 +2001,35 @@ func (m *HostSubnet) Unmarshal(dAtA []byte) error { } m.Subnet = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field EgressIPs", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.EgressIPs = append(m.EgressIPs, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -2196,6 +2269,35 @@ func (m *NetNamespace) Unmarshal(dAtA []byte) error { break } } + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field EgressIPs", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.EgressIPs = append(m.EgressIPs, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -2438,62 +2540,63 @@ func init() { } var fileDescriptorGenerated = []byte{ - // 898 bytes of a gzipped FileDescriptorProto + // 927 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x56, 0x4f, 0x6f, 0xe3, 0x44, - 0x14, 0xcf, 0xa4, 0x69, 0x77, 0x3b, 0xed, 0xa6, 0xd5, 0x2c, 0x62, 0xad, 0x22, 0xb9, 0x91, 0x91, - 0x50, 0x10, 0xc2, 0xa6, 0x05, 0xc1, 0x9e, 0x40, 0x72, 0x5b, 0xed, 0x56, 0x2a, 0x21, 0x9a, 0xec, - 0x09, 0x71, 0xc0, 0x75, 0xde, 0x3a, 0xb3, 0x75, 0x6c, 0xe3, 0x99, 0x04, 0x72, 0x00, 0xf1, 0x0d, - 0x40, 0xe2, 0x0b, 0x71, 0xec, 0x71, 0x8f, 0x3d, 0xa0, 0x6a, 0x1b, 0x4e, 0xc0, 0x37, 0xe0, 0x84, - 0x66, 0x3c, 0x71, 0xec, 0xd6, 0xd1, 0xa2, 0xaa, 0x9b, 0x53, 0x3b, 0xf3, 0x7b, 0x7f, 0x7e, 0xef, - 0xfd, 0xe6, 0x3d, 0x07, 0x3f, 0x09, 0x98, 0x18, 0x8c, 0x4e, 0x6d, 0x3f, 0x1e, 0x3a, 0x71, 0x02, - 0x11, 0x1f, 0xb0, 0xe7, 0xc2, 0x89, 0x53, 0x16, 0xb0, 0xc8, 0x49, 0xce, 0x02, 0x27, 0x02, 0xf1, - 0x7d, 0x9c, 0x9e, 0x39, 0x5e, 0xc2, 0x78, 0x7e, 0x18, 0xef, 0x39, 0x01, 0x44, 0x90, 0x7a, 0x02, - 0xfa, 0x76, 0x92, 0xc6, 0x22, 0x26, 0x9f, 0xcd, 0x03, 0xd9, 0x79, 0x20, 0x3b, 0x0b, 0x64, 0x27, - 0x67, 0x81, 0xad, 0x7d, 0x6d, 0x19, 0x28, 0x3f, 0x8c, 0xf7, 0x76, 0x3e, 0x2c, 0x30, 0x08, 0xe2, - 0x20, 0x76, 0x54, 0xbc, 0xd3, 0xd1, 0x73, 0x75, 0x52, 0x07, 0xf5, 0x5f, 0x96, 0x67, 0xe7, 0x93, - 0xb3, 0xc7, 0xdc, 0x66, 0xb1, 0xe4, 0x33, 0xf4, 0xfc, 0x01, 0x8b, 0x20, 0x9d, 0x28, 0xa2, 0x8a, - 0xe0, 0x10, 0x84, 0x57, 0xc1, 0x6e, 0xc7, 0x59, 0xe4, 0x95, 0x8e, 0x22, 0xc1, 0x86, 0x70, 0xc3, - 0xe1, 0xd3, 0xd7, 0x39, 0x70, 0x7f, 0x00, 0x43, 0xef, 0x86, 0xdf, 0xc7, 0x8b, 0xfc, 0x46, 0x82, - 0x85, 0x0e, 0x8b, 0x04, 0x17, 0xe9, 0x75, 0x27, 0xeb, 0xaf, 0x15, 0xdc, 0x3c, 0x08, 0x47, 0x5c, - 0x40, 0xda, 0xc9, 0x1a, 0x43, 0xbe, 0xc5, 0xf7, 0x65, 0x2d, 0x7d, 0x4f, 0x78, 0x06, 0x6a, 0xa1, - 0xf6, 0xc6, 0xfe, 0x47, 0x76, 0x16, 0xda, 0x2e, 0x86, 0x56, 0x9d, 0x55, 0x1d, 0x95, 0xd6, 0xf6, - 0x78, 0xcf, 0xfe, 0xea, 0xf4, 0x05, 0xf8, 0xe2, 0x4b, 0x10, 0x9e, 0x4b, 0xce, 0x2f, 0x77, 0x6b, - 0xd3, 0xcb, 0x5d, 0x3c, 0xbf, 0xa3, 0x79, 0x54, 0xf2, 0x3e, 0xbe, 0xa7, 0x55, 0x30, 0xea, 0x2d, - 0xd4, 0x5e, 0x77, 0xb7, 0xb4, 0xf9, 0x3d, 0xcd, 0x81, 0xce, 0x70, 0x72, 0x88, 0xb7, 0x07, 0x31, - 0x17, 0x7c, 0x74, 0x1a, 0x81, 0x08, 0x21, 0x0a, 0xc4, 0xc0, 0x58, 0x69, 0xa1, 0xf6, 0x03, 0xd7, - 0xd0, 0x3e, 0xdb, 0x4f, 0x63, 0x2e, 0x7a, 0x0a, 0x3f, 0x51, 0x38, 0xbd, 0xe1, 0x41, 0x3e, 0xc7, - 0x4d, 0x0e, 0xe9, 0x98, 0xf9, 0xa0, 0x13, 0x18, 0x0d, 0x95, 0xf7, 0x6d, 0x1d, 0xa3, 0xd9, 0x2b, - 0xa1, 0xf4, 0x9a, 0x35, 0xd9, 0xc7, 0x38, 0x09, 0x47, 0x01, 0x8b, 0x3a, 0xde, 0x10, 0x8c, 0x55, - 0xe5, 0x9b, 0x97, 0xd8, 0xcd, 0x11, 0x5a, 0xb0, 0x22, 0xbf, 0x20, 0xbc, 0xe5, 0x97, 0x3a, 0xcb, - 0x8d, 0xb5, 0xd6, 0x4a, 0x7b, 0x63, 0xff, 0xc4, 0xbe, 0xe5, 0x83, 0xb5, 0xcb, 0x4a, 0x1d, 0x45, - 0x22, 0x9d, 0xb8, 0x8f, 0x34, 0x8f, 0xad, 0x32, 0xc8, 0xe9, 0xf5, 0xec, 0xd6, 0x8f, 0xf8, 0x61, - 0x45, 0x00, 0xd2, 0xc2, 0x0d, 0x9f, 0xf5, 0x53, 0xa5, 0xf5, 0xba, 0xbb, 0xa9, 0xc3, 0x35, 0x0e, - 0x8e, 0x0f, 0x29, 0x55, 0xc8, 0x4c, 0x84, 0x62, 0x93, 0x95, 0x70, 0xaf, 0x15, 0xa1, 0x78, 0x63, - 0xbd, 0x42, 0x98, 0x94, 0xf3, 0x9f, 0x30, 0x2e, 0xc8, 0x37, 0x37, 0x9e, 0x9b, 0xfd, 0xff, 0x9e, - 0x9b, 0xf4, 0x56, 0x8f, 0x6d, 0x5b, 0x93, 0xb8, 0x3f, 0xbb, 0x29, 0x3c, 0xb5, 0x10, 0xaf, 0x32, - 0x01, 0x43, 0x6e, 0xd4, 0x55, 0xeb, 0x9f, 0xdc, 0x51, 0xeb, 0xdd, 0x07, 0x3a, 0xe7, 0xea, 0xb1, - 0x8c, 0x4e, 0xb3, 0x24, 0xd6, 0x3f, 0x08, 0x3f, 0x3c, 0x0a, 0x52, 0xe0, 0x5c, 0xdb, 0x75, 0xe3, - 0x90, 0xf9, 0x93, 0x25, 0x8c, 0x54, 0x8a, 0x1b, 0x3c, 0x01, 0x5f, 0xc9, 0xb2, 0xb1, 0xdf, 0xbd, - 0x75, 0x99, 0x15, 0xec, 0x7b, 0x09, 0xf8, 0xf3, 0x67, 0x21, 0x4f, 0x54, 0xe5, 0xb2, 0xfe, 0x46, - 0xf8, 0x51, 0x85, 0xfd, 0x12, 0x54, 0xfd, 0xae, 0xac, 0xea, 0xc9, 0x5d, 0x96, 0xbb, 0x40, 0xda, - 0x9f, 0x2a, 0x6b, 0xed, 0x02, 0xa4, 0xe4, 0x31, 0xde, 0x94, 0x63, 0xd2, 0x83, 0x10, 0x7c, 0x11, - 0xcf, 0x06, 0xe9, 0x2d, 0x1d, 0x66, 0x53, 0x0e, 0xd2, 0x0c, 0xa3, 0x25, 0x4b, 0xb9, 0x08, 0xfb, - 0x11, 0x57, 0x4b, 0xe5, 0xda, 0x22, 0x3c, 0xec, 0xf4, 0xd4, 0x46, 0x99, 0xe1, 0xd6, 0x79, 0x75, - 0xb3, 0xe9, 0x28, 0x04, 0xf2, 0x05, 0x6e, 0x88, 0x49, 0x02, 0x3a, 0xf1, 0x07, 0x33, 0xa9, 0x9e, - 0x4d, 0x12, 0xf8, 0xf7, 0x72, 0xf7, 0x9d, 0x05, 0x6e, 0x12, 0xa6, 0xca, 0x91, 0x84, 0xb8, 0x2e, - 0xe2, 0x37, 0xf1, 0x76, 0x64, 0x7f, 0x5c, 0xac, 0x09, 0xd5, 0x9f, 0xc5, 0xb4, 0x2e, 0x62, 0xeb, - 0xb7, 0xea, 0x52, 0xe4, 0xcb, 0x22, 0x3f, 0xe0, 0x35, 0x50, 0x90, 0x81, 0x94, 0xb4, 0x77, 0xca, - 0x46, 0x56, 0xed, 0x36, 0x35, 0x9b, 0xb5, 0xcc, 0x80, 0xea, 0x7c, 0x72, 0x3d, 0xe1, 0xf9, 0x16, - 0x5b, 0xc2, 0xc8, 0xb6, 0x70, 0x43, 0xee, 0x48, 0xad, 0x7c, 0x3e, 0x60, 0x92, 0x03, 0x55, 0x08, - 0x79, 0x0f, 0xaf, 0xc9, 0xbf, 0xc7, 0x5d, 0xf5, 0xc9, 0x5b, 0x9f, 0x53, 0x7f, 0xaa, 0x6e, 0xa9, - 0x46, 0xa5, 0x5d, 0xf6, 0xb9, 0xd3, 0x9f, 0xb5, 0xdc, 0x2e, 0xab, 0x85, 0x6a, 0xd4, 0xba, 0x40, - 0xb8, 0x59, 0x58, 0xd4, 0x6f, 0x7e, 0x4e, 0x07, 0xe5, 0x39, 0x3d, 0xb8, 0xb5, 0x98, 0x73, 0xd6, - 0x0b, 0xc6, 0xf3, 0x77, 0x84, 0x37, 0x3b, 0x20, 0xe4, 0xa8, 0xf0, 0xc4, 0xf3, 0x61, 0x69, 0xbf, - 0x62, 0xa2, 0x8a, 0xe1, 0xd5, 0x44, 0xe8, 0x0c, 0x27, 0xef, 0xe2, 0xd5, 0x08, 0x04, 0xeb, 0xeb, - 0x9f, 0x2e, 0x79, 0x09, 0x1d, 0x10, 0xc7, 0x87, 0x34, 0xc3, 0xac, 0x3f, 0x10, 0xde, 0x2e, 0x96, - 0xb0, 0x04, 0x7d, 0x5e, 0x94, 0xf5, 0x39, 0xba, 0xb5, 0x3e, 0x45, 0xde, 0xd5, 0x0a, 0xb9, 0xed, - 0xf3, 0x2b, 0xb3, 0xf6, 0xf2, 0xca, 0xac, 0x5d, 0x5c, 0x99, 0xb5, 0x9f, 0xa7, 0x26, 0x3a, 0x9f, - 0x9a, 0xe8, 0xe5, 0xd4, 0x44, 0x17, 0x53, 0x13, 0xbd, 0x9a, 0x9a, 0xe8, 0xd7, 0x3f, 0xcd, 0xda, - 0xd7, 0xf5, 0xf1, 0xde, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x3c, 0x1a, 0xea, 0x2e, 0x19, 0x0c, - 0x00, 0x00, + 0x14, 0xcf, 0xa4, 0x49, 0x77, 0x33, 0x6d, 0xd3, 0x6a, 0x16, 0xb1, 0x56, 0x91, 0xdc, 0xc8, 0x48, + 0x28, 0x68, 0x85, 0x4d, 0x0b, 0x82, 0x3d, 0x81, 0xe4, 0xb6, 0xda, 0x8d, 0x54, 0x42, 0x34, 0xd9, + 0x13, 0xe2, 0x80, 0xeb, 0xcc, 0x3a, 0xb3, 0x75, 0x6c, 0xe3, 0x99, 0x04, 0x72, 0x00, 0xf1, 0x05, + 0x10, 0x48, 0x7c, 0xa9, 0x1e, 0xf7, 0xd8, 0x03, 0xaa, 0xb6, 0xe1, 0x04, 0x88, 0x2f, 0xc0, 0x09, + 0xcd, 0x78, 0xec, 0xd8, 0xad, 0xa3, 0xa2, 0xa8, 0xcd, 0xa9, 0x9d, 0xf9, 0xbd, 0x3f, 0xbf, 0xf7, + 0x7e, 0xef, 0x8d, 0x03, 0x9f, 0x79, 0x94, 0x0f, 0xc7, 0xa7, 0xa6, 0x1b, 0x8e, 0xac, 0x30, 0x22, + 0x01, 0x1b, 0xd2, 0x97, 0xdc, 0x0a, 0x63, 0xea, 0xd1, 0xc0, 0x8a, 0xce, 0x3c, 0x2b, 0x20, 0xfc, + 0xbb, 0x30, 0x3e, 0xb3, 0x9c, 0x88, 0xb2, 0xec, 0x30, 0xd9, 0xb7, 0x3c, 0x12, 0x90, 0xd8, 0xe1, + 0x64, 0x60, 0x46, 0x71, 0xc8, 0x43, 0xf4, 0xe9, 0x3c, 0x90, 0x99, 0x05, 0x32, 0x93, 0x40, 0x66, + 0x74, 0xe6, 0x99, 0xca, 0xd7, 0x14, 0x81, 0xb2, 0xc3, 0x64, 0x7f, 0xf7, 0x83, 0x1c, 0x03, 0x2f, + 0xf4, 0x42, 0x4b, 0xc6, 0x3b, 0x1d, 0xbf, 0x94, 0x27, 0x79, 0x90, 0xff, 0x25, 0x79, 0x76, 0x3f, + 0x3e, 0x7b, 0xca, 0x4c, 0x1a, 0x0a, 0x3e, 0x23, 0xc7, 0x1d, 0xd2, 0x80, 0xc4, 0x53, 0x49, 0x54, + 0x12, 0x1c, 0x11, 0xee, 0x94, 0xb0, 0xdb, 0xb5, 0x16, 0x79, 0xc5, 0xe3, 0x80, 0xd3, 0x11, 0xb9, + 0xe1, 0xf0, 0xc9, 0x6d, 0x0e, 0xcc, 0x1d, 0x92, 0x91, 0x73, 0xc3, 0xef, 0xa3, 0x45, 0x7e, 0x63, + 0x4e, 0x7d, 0x8b, 0x06, 0x9c, 0xf1, 0xf8, 0xba, 0x93, 0xf1, 0xe7, 0x1a, 0x6c, 0x1e, 0xfa, 0x63, + 0xc6, 0x49, 0xdc, 0x4d, 0x1a, 0x83, 0xbe, 0x81, 0x0f, 0x45, 0x2d, 0x03, 0x87, 0x3b, 0x1a, 0x68, + 0x81, 0xf6, 0xc6, 0xc1, 0x87, 0x66, 0x12, 0xda, 0xcc, 0x87, 0x96, 0x9d, 0x95, 0x1d, 0x15, 0xd6, + 0xe6, 0x64, 0xdf, 0xfc, 0xf2, 0xf4, 0x15, 0x71, 0xf9, 0x17, 0x84, 0x3b, 0x36, 0x3a, 0xbf, 0xdc, + 0xab, 0xcc, 0x2e, 0xf7, 0xe0, 0xfc, 0x0e, 0x67, 0x51, 0xd1, 0xfb, 0xf0, 0x81, 0x52, 0x41, 0xab, + 0xb6, 0x40, 0xbb, 0x61, 0x6f, 0x2b, 0xf3, 0x07, 0x8a, 0x03, 0x4e, 0x71, 0x74, 0x04, 0x77, 0x86, + 0x21, 0xe3, 0x6c, 0x7c, 0x1a, 0x10, 0xee, 0x93, 0xc0, 0xe3, 0x43, 0x6d, 0xad, 0x05, 0xda, 0x5b, + 0xb6, 0xa6, 0x7c, 0x76, 0x9e, 0x87, 0x8c, 0xf7, 0x25, 0x7e, 0x22, 0x71, 0x7c, 0xc3, 0x03, 0x7d, + 0x06, 0x9b, 0x8c, 0xc4, 0x13, 0xea, 0x12, 0x95, 0x40, 0xab, 0xc9, 0xbc, 0x6f, 0xab, 0x18, 0xcd, + 0x7e, 0x01, 0xc5, 0xd7, 0xac, 0xd1, 0x01, 0x84, 0x91, 0x3f, 0xf6, 0x68, 0xd0, 0x75, 0x46, 0x44, + 0xab, 0x4b, 0xdf, 0xac, 0xc4, 0x5e, 0x86, 0xe0, 0x9c, 0x15, 0xfa, 0x05, 0xc0, 0x6d, 0xb7, 0xd0, + 0x59, 0xa6, 0xad, 0xb7, 0xd6, 0xda, 0x1b, 0x07, 0x27, 0xe6, 0x92, 0x03, 0x6b, 0x16, 0x95, 0x3a, + 0x0e, 0x78, 0x3c, 0xb5, 0x1f, 0x2b, 0x1e, 0xdb, 0x45, 0x90, 0xe1, 0xeb, 0xd9, 0x8d, 0x1f, 0xe0, + 0xa3, 0x92, 0x00, 0xa8, 0x05, 0x6b, 0x2e, 0x1d, 0xc4, 0x52, 0xeb, 0x86, 0xbd, 0xa9, 0xc2, 0xd5, + 0x0e, 0x3b, 0x47, 0x18, 0x4b, 0x24, 0x15, 0x21, 0xdf, 0x64, 0x29, 0xdc, 0xad, 0x22, 0xe4, 0x6f, + 0x8c, 0x37, 0x00, 0xa2, 0x62, 0xfe, 0x13, 0xca, 0x38, 0xfa, 0xfa, 0xc6, 0xb8, 0x99, 0xff, 0x6f, + 0xdc, 0x84, 0xb7, 0x1c, 0xb6, 0x1d, 0x45, 0xe2, 0x61, 0x7a, 0x93, 0x1b, 0x35, 0x1f, 0xd6, 0x29, + 0x27, 0x23, 0xa6, 0x55, 0x65, 0xeb, 0x9f, 0xdd, 0x51, 0xeb, 0xed, 0x2d, 0x95, 0xb3, 0xde, 0x11, + 0xd1, 0x71, 0x92, 0xc4, 0xf8, 0x1b, 0xc0, 0x47, 0xc7, 0x5e, 0x4c, 0x18, 0x53, 0x76, 0xbd, 0xd0, + 0xa7, 0xee, 0x74, 0x05, 0x2b, 0x15, 0xc3, 0x1a, 0x8b, 0x88, 0x2b, 0x65, 0xd9, 0x38, 0xe8, 0x2d, + 0x5d, 0x66, 0x09, 0xfb, 0x7e, 0x44, 0xdc, 0xf9, 0x58, 0x88, 0x13, 0x96, 0xb9, 0x8c, 0xbf, 0x00, + 0x7c, 0x5c, 0x62, 0xbf, 0x02, 0x55, 0xbf, 0x2d, 0xaa, 0x7a, 0x72, 0x97, 0xe5, 0x2e, 0x90, 0xf6, + 0xc7, 0xd2, 0x5a, 0x7b, 0x84, 0xc4, 0xe8, 0x29, 0xdc, 0x14, 0x6b, 0xd2, 0x27, 0x3e, 0x71, 0x79, + 0x98, 0x2e, 0xd2, 0x5b, 0x2a, 0xcc, 0xa6, 0x58, 0xa4, 0x14, 0xc3, 0x05, 0x4b, 0xf1, 0x10, 0x0e, + 0x02, 0x26, 0x1f, 0x95, 0x6b, 0x0f, 0xe1, 0x51, 0xb7, 0x2f, 0x5f, 0x94, 0x14, 0x37, 0xce, 0xcb, + 0x9b, 0x8d, 0xc7, 0x3e, 0x41, 0x9f, 0xc3, 0x1a, 0x9f, 0x46, 0x44, 0x25, 0x7e, 0x92, 0x4a, 0xf5, + 0x62, 0x1a, 0x91, 0x7f, 0x2f, 0xf7, 0xde, 0x59, 0xe0, 0x26, 0x60, 0x2c, 0x1d, 0x91, 0x0f, 0xab, + 0x3c, 0xbc, 0x8f, 0xd9, 0x11, 0xfd, 0xb1, 0xa1, 0x22, 0x54, 0x7d, 0x11, 0xe2, 0x2a, 0x0f, 0x8d, + 0xdf, 0xca, 0x4b, 0x11, 0x93, 0x85, 0xbe, 0x87, 0xeb, 0x44, 0x42, 0x1a, 0x90, 0xd2, 0xde, 0x29, + 0x1b, 0x51, 0xb5, 0xdd, 0x54, 0x6c, 0xd6, 0x13, 0x03, 0xac, 0xf2, 0x19, 0x3f, 0x57, 0x21, 0x9c, + 0xbf, 0x62, 0x2b, 0x58, 0xd9, 0x16, 0xac, 0x89, 0x37, 0x52, 0x29, 0x9f, 0x2d, 0x98, 0xe0, 0x80, + 0x25, 0x82, 0xde, 0x83, 0xeb, 0xe2, 0x6f, 0xa7, 0x27, 0x3f, 0x79, 0x8d, 0x39, 0xf5, 0xe7, 0xf2, + 0x16, 0x2b, 0x54, 0xd8, 0x25, 0x9f, 0x3b, 0xf5, 0x59, 0xcb, 0xec, 0x92, 0x5a, 0xb0, 0x42, 0xd1, + 0x13, 0xd8, 0x48, 0x8a, 0xed, 0xf4, 0x98, 0x56, 0x6f, 0xad, 0xb5, 0x1b, 0xf6, 0xd6, 0xec, 0x72, + 0xaf, 0x71, 0x9c, 0x5e, 0xe2, 0x39, 0x6e, 0x5c, 0x00, 0xd8, 0xcc, 0xbd, 0xea, 0xf7, 0xbf, 0xd4, + 0xc3, 0xe2, 0x52, 0x1f, 0x2e, 0xad, 0xfc, 0x9c, 0xf5, 0x82, 0x5d, 0xfe, 0x07, 0xc0, 0xcd, 0x2e, + 0xe1, 0x62, 0xaf, 0x58, 0xe4, 0xb8, 0x64, 0x65, 0x3f, 0x79, 0x82, 0x92, 0x4d, 0x57, 0x44, 0x70, + 0x8a, 0xa3, 0x77, 0x61, 0x3d, 0x20, 0x9c, 0x0e, 0xd4, 0xef, 0x9c, 0xac, 0x84, 0x2e, 0xe1, 0x9d, + 0x23, 0x9c, 0x60, 0x45, 0x29, 0x6b, 0xb7, 0x48, 0xf9, 0x3b, 0x80, 0x3b, 0xf9, 0x7a, 0x57, 0x20, + 0xe6, 0xab, 0xa2, 0x98, 0xc7, 0x4b, 0x8b, 0x99, 0xe7, 0x5d, 0x2e, 0xa7, 0xdd, 0x3e, 0xbf, 0xd2, + 0x2b, 0xaf, 0xaf, 0xf4, 0xca, 0xc5, 0x95, 0x5e, 0xf9, 0x69, 0xa6, 0x83, 0xf3, 0x99, 0x0e, 0x5e, + 0xcf, 0x74, 0x70, 0x31, 0xd3, 0xc1, 0x9b, 0x99, 0x0e, 0x7e, 0xfd, 0x43, 0xaf, 0x7c, 0x55, 0x9d, + 0xec, 0xff, 0x17, 0x00, 0x00, 0xff, 0xff, 0x84, 0x5e, 0xe2, 0xdc, 0x73, 0x0c, 0x00, 0x00, } diff --git a/pkg/network/apis/network/v1/generated.proto b/pkg/network/apis/network/v1/generated.proto index ad7917a65290..a14c8f0e702e 100644 --- a/pkg/network/apis/network/v1/generated.proto +++ b/pkg/network/apis/network/v1/generated.proto @@ -114,6 +114,9 @@ message HostSubnet { // Subnet is the CIDR range of the overlay network assigned to the node for its pods optional string subnet = 4; + + // EgressIPs is the list of automatic egress IP addresses currently hosted by this node + repeated string egressIPs = 5; } // HostSubnetList is a collection of HostSubnets @@ -137,6 +140,10 @@ message NetNamespace { // NetID is the network identifier of the network namespace assigned to each overlay network packet. This can be manipulated with the "oc adm pod-network" commands. optional uint32 netid = 3; + + // EgressIPs is a list of reserved IPs that will be used as the source for external traffic coming from pods in this namespace. (If empty, external traffic will be masqueraded to Node IPs.) + // +optional + repeated string egressIPs = 4; } // NetNamespaceList is a collection of NetNamespaces diff --git a/pkg/network/apis/network/v1/swagger_doc.go b/pkg/network/apis/network/v1/swagger_doc.go index 15d4247f711d..3e94b825f0cd 100644 --- a/pkg/network/apis/network/v1/swagger_doc.go +++ b/pkg/network/apis/network/v1/swagger_doc.go @@ -89,11 +89,12 @@ func (EgressNetworkPolicySpec) SwaggerDoc() map[string]string { } var map_HostSubnet = map[string]string{ - "": "HostSubnet describes the container subnet network on a node. The HostSubnet object must have the same name as the Node object it corresponds to.", - "metadata": "Standard object's metadata.", - "host": "Host is the name of the node. (This is the same as the object's name, but both fields must be set.)", - "hostIP": "HostIP is the IP address to be used as a VTEP by other nodes in the overlay network", - "subnet": "Subnet is the CIDR range of the overlay network assigned to the node for its pods", + "": "HostSubnet describes the container subnet network on a node. The HostSubnet object must have the same name as the Node object it corresponds to.", + "metadata": "Standard object's metadata.", + "host": "Host is the name of the node. (This is the same as the object's name, but both fields must be set.)", + "hostIP": "HostIP is the IP address to be used as a VTEP by other nodes in the overlay network", + "subnet": "Subnet is the CIDR range of the overlay network assigned to the node for its pods", + "egressIPs": "EgressIPs is the list of automatic egress IP addresses currently hosted by this node", } func (HostSubnet) SwaggerDoc() map[string]string { @@ -111,10 +112,11 @@ func (HostSubnetList) SwaggerDoc() map[string]string { } var map_NetNamespace = map[string]string{ - "": "NetNamespace describes a single isolated network. When using the redhat/openshift-ovs-multitenant plugin, every Namespace will have a corresponding NetNamespace object with the same name. (When using redhat/openshift-ovs-subnet, NetNamespaces are not used.)", - "metadata": "Standard object's metadata.", - "netname": "NetName is the name of the network namespace. (This is the same as the object's name, but both fields must be set.)", - "netid": "NetID is the network identifier of the network namespace assigned to each overlay network packet. This can be manipulated with the \"oc adm pod-network\" commands.", + "": "NetNamespace describes a single isolated network. When using the redhat/openshift-ovs-multitenant plugin, every Namespace will have a corresponding NetNamespace object with the same name. (When using redhat/openshift-ovs-subnet, NetNamespaces are not used.)", + "metadata": "Standard object's metadata.", + "netname": "NetName is the name of the network namespace. (This is the same as the object's name, but both fields must be set.)", + "netid": "NetID is the network identifier of the network namespace assigned to each overlay network packet. This can be manipulated with the \"oc adm pod-network\" commands.", + "egressIPs": "EgressIPs is a list of reserved IPs that will be used as the source for external traffic coming from pods in this namespace. (If empty, external traffic will be masqueraded to Node IPs.)", } func (NetNamespace) SwaggerDoc() map[string]string { diff --git a/pkg/network/apis/network/v1/types.go b/pkg/network/apis/network/v1/types.go index 95c77ff3d3de..1735decf729a 100644 --- a/pkg/network/apis/network/v1/types.go +++ b/pkg/network/apis/network/v1/types.go @@ -68,6 +68,9 @@ type HostSubnet struct { HostIP string `json:"hostIP" protobuf:"bytes,3,opt,name=hostIP"` // Subnet is the CIDR range of the overlay network assigned to the node for its pods Subnet string `json:"subnet" protobuf:"bytes,4,opt,name=subnet"` + + // EgressIPs is the list of automatic egress IP addresses currently hosted by this node + EgressIPs []string `json:"egressIPs" protobuf:"bytes,5,rep,name=egressIPs"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -97,6 +100,10 @@ type NetNamespace struct { NetName string `json:"netname" protobuf:"bytes,2,opt,name=netname"` // NetID is the network identifier of the network namespace assigned to each overlay network packet. This can be manipulated with the "oc adm pod-network" commands. NetID uint32 `json:"netid" protobuf:"varint,3,opt,name=netid"` + + // EgressIPs is a list of reserved IPs that will be used as the source for external traffic coming from pods in this namespace. (If empty, external traffic will be masqueraded to Node IPs.) + // +optional + EgressIPs []string `json:"egressIPs" protobuf:"bytes,4,rep,name=egressIPs"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/network/apis/network/v1/zz_generated.conversion.go b/pkg/network/apis/network/v1/zz_generated.conversion.go index 8d361e463049..6ee0428c9948 100644 --- a/pkg/network/apis/network/v1/zz_generated.conversion.go +++ b/pkg/network/apis/network/v1/zz_generated.conversion.go @@ -273,6 +273,7 @@ func autoConvert_v1_HostSubnet_To_network_HostSubnet(in *HostSubnet, out *networ out.Host = in.Host out.HostIP = in.HostIP out.Subnet = in.Subnet + out.EgressIPs = *(*[]string)(unsafe.Pointer(&in.EgressIPs)) return nil } @@ -286,6 +287,11 @@ func autoConvert_network_HostSubnet_To_v1_HostSubnet(in *network.HostSubnet, out out.Host = in.Host out.HostIP = in.HostIP out.Subnet = in.Subnet + if in.EgressIPs == nil { + out.EgressIPs = make([]string, 0) + } else { + out.EgressIPs = *(*[]string)(unsafe.Pointer(&in.EgressIPs)) + } return nil } @@ -324,6 +330,7 @@ func autoConvert_v1_NetNamespace_To_network_NetNamespace(in *NetNamespace, out * out.ObjectMeta = in.ObjectMeta out.NetName = in.NetName out.NetID = in.NetID + out.EgressIPs = *(*[]string)(unsafe.Pointer(&in.EgressIPs)) return nil } @@ -336,6 +343,11 @@ func autoConvert_network_NetNamespace_To_v1_NetNamespace(in *network.NetNamespac out.ObjectMeta = in.ObjectMeta out.NetName = in.NetName out.NetID = in.NetID + if in.EgressIPs == nil { + out.EgressIPs = make([]string, 0) + } else { + out.EgressIPs = *(*[]string)(unsafe.Pointer(&in.EgressIPs)) + } return nil } diff --git a/pkg/network/apis/network/v1/zz_generated.deepcopy.go b/pkg/network/apis/network/v1/zz_generated.deepcopy.go index 6c4c5f1e0137..6f9c8d4d8647 100644 --- a/pkg/network/apis/network/v1/zz_generated.deepcopy.go +++ b/pkg/network/apis/network/v1/zz_generated.deepcopy.go @@ -166,6 +166,11 @@ func DeepCopy_v1_HostSubnet(in interface{}, out interface{}, c *conversion.Clone } else { out.ObjectMeta = *newVal.(*meta_v1.ObjectMeta) } + if in.EgressIPs != nil { + in, out := &in.EgressIPs, &out.EgressIPs + *out = make([]string, len(*in)) + copy(*out, *in) + } return nil } } @@ -200,6 +205,11 @@ func DeepCopy_v1_NetNamespace(in interface{}, out interface{}, c *conversion.Clo } else { out.ObjectMeta = *newVal.(*meta_v1.ObjectMeta) } + if in.EgressIPs != nil { + in, out := &in.EgressIPs, &out.EgressIPs + *out = make([]string, len(*in)) + copy(*out, *in) + } return nil } } diff --git a/pkg/network/apis/network/validation/validation.go b/pkg/network/apis/network/validation/validation.go index 37bb93ce983d..3aaa87423756 100644 --- a/pkg/network/apis/network/validation/validation.go +++ b/pkg/network/apis/network/validation/validation.go @@ -102,8 +102,6 @@ func ValidateClusterNetworkUpdate(obj *networkapi.ClusterNetwork, old *networkap return allErrs } -// ValidateHostSubnet tests fields for the host subnet, the host should be a network resolvable string, -// and subnet should be a valid CIDR func ValidateHostSubnet(hs *networkapi.HostSubnet) field.ErrorList { allErrs := validation.ValidateObjectMeta(&hs.ObjectMeta, false, path.ValidatePathSegmentName, field.NewPath("metadata")) @@ -125,6 +123,13 @@ func ValidateHostSubnet(hs *networkapi.HostSubnet) field.ErrorList { if net.ParseIP(hs.HostIP) == nil { allErrs = append(allErrs, field.Invalid(field.NewPath("hostIP"), hs.HostIP, "invalid IP address")) } + + for i, egressIP := range hs.EgressIPs { + if net.ParseIP(egressIP) == nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("egressIPs").Index(i), egressIP, "invalid IP address")) + } + } + return allErrs } @@ -150,6 +155,13 @@ func ValidateNetNamespace(netnamespace *networkapi.NetNamespace) field.ErrorList if err := network.ValidVNID(netnamespace.NetID); err != nil { allErrs = append(allErrs, field.Invalid(field.NewPath("netid"), netnamespace.NetID, err.Error())) } + + for i, ip := range netnamespace.EgressIPs { + if net.ParseIP(ip) == nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("egressIPs").Index(i), ip, "invalid IP address")) + } + } + return allErrs } diff --git a/pkg/network/apis/network/validation/validation_test.go b/pkg/network/apis/network/validation/validation_test.go index 5cbf5c522d33..d1cae4d25e0a 100644 --- a/pkg/network/apis/network/validation/validation_test.go +++ b/pkg/network/apis/network/validation/validation_test.go @@ -299,6 +299,38 @@ func TestValidateHostSubnet(t *testing.T) { }, expectedErrors: 1, }, + { + name: "Good one with EgressIPs", + hs: &networkapi.HostSubnet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc.def.com", + }, + Host: "abc.def.com", + HostIP: "10.20.30.40", + Subnet: "8.8.8.0/24", + EgressIPs: []string{ + "192.168.1.99", + "192.168.2.42", + }, + }, + expectedErrors: 0, + }, + { + name: "Malformed EgressIPs", + hs: &networkapi.HostSubnet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc.def.com", + }, + Host: "abc.def.com", + HostIP: "10.20.30.40", + Subnet: "8.8.8.0/24", + EgressIPs: []string{ + "192.168.1.0/24", + "bob", + }, + }, + expectedErrors: 2, + }, } for _, tc := range tests { @@ -310,6 +342,91 @@ func TestValidateHostSubnet(t *testing.T) { } } +func TestValidateNetNamespace(t *testing.T) { + tests := []struct { + name string + netns *networkapi.NetNamespace + expectedErrors int + }{ + { + name: "Good one", + netns: &networkapi.NetNamespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + NetName: "abc", + NetID: 12345, + }, + expectedErrors: 0, + }, + { + name: "Bad NetName", + netns: &networkapi.NetNamespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + NetName: "wrong", + NetID: 12345, + }, + expectedErrors: 1, + }, + { + name: "NetID too large", + netns: &networkapi.NetNamespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + NetName: "abc", + NetID: 16777217, + }, + expectedErrors: 1, + }, + { + name: "NetID in reserved range", + netns: &networkapi.NetNamespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + NetName: "abc", + NetID: 5, + }, + expectedErrors: 1, + }, + { + name: "Good with EgressIPs", + netns: &networkapi.NetNamespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + NetName: "abc", + NetID: 12345, + EgressIPs: []string{"192.168.1.5", "192.168.1.7"}, + }, + expectedErrors: 0, + }, + { + name: "Bad EgressIPs", + netns: &networkapi.NetNamespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + }, + NetName: "abc", + NetID: 12345, + EgressIPs: []string{"example.com", ""}, + }, + expectedErrors: 2, + }, + } + + for _, tc := range tests { + errs := ValidateNetNamespace(tc.netns) + + if len(errs) != tc.expectedErrors { + t.Errorf("Test case %s expected %d error(s), got %d. %v", tc.name, tc.expectedErrors, len(errs), errs) + } + } +} + func TestValidateEgressNetworkPolicy(t *testing.T) { tests := []struct { name string diff --git a/pkg/network/apis/network/zz_generated.deepcopy.go b/pkg/network/apis/network/zz_generated.deepcopy.go index 4b75419b50b7..db7af1a53474 100644 --- a/pkg/network/apis/network/zz_generated.deepcopy.go +++ b/pkg/network/apis/network/zz_generated.deepcopy.go @@ -166,6 +166,11 @@ func DeepCopy_network_HostSubnet(in interface{}, out interface{}, c *conversion. } else { out.ObjectMeta = *newVal.(*v1.ObjectMeta) } + if in.EgressIPs != nil { + in, out := &in.EgressIPs, &out.EgressIPs + *out = make([]string, len(*in)) + copy(*out, *in) + } return nil } } @@ -200,6 +205,11 @@ func DeepCopy_network_NetNamespace(in interface{}, out interface{}, c *conversio } else { out.ObjectMeta = *newVal.(*v1.ObjectMeta) } + if in.EgressIPs != nil { + in, out := &in.EgressIPs, &out.EgressIPs + *out = make([]string, len(*in)) + copy(*out, *in) + } return nil } } diff --git a/pkg/network/node/egressip.go b/pkg/network/node/egressip.go new file mode 100644 index 000000000000..de5d61c88fea --- /dev/null +++ b/pkg/network/node/egressip.go @@ -0,0 +1,302 @@ +package node + +import ( + "fmt" + "net" + "sync" + "syscall" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/util/sets" + utilwait "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/cache" + + networkapi "github.com/openshift/origin/pkg/network/apis/network" + "github.com/openshift/origin/pkg/network/common" + networkclient "github.com/openshift/origin/pkg/network/generated/internalclientset" + + "github.com/vishvananda/netlink" +) + +type nodeEgress struct { + nodeIP string + egressIPs sets.String +} + +type namespaceEgress struct { + vnid uint32 + + // claimedIP is the egress IP it wants (NetNamespace.EgressIP[0]), or "" for none + claimedIP string + // assignedIP is an egress IP actually in use on nodeIP + assignedIP string + nodeIP string +} + +type egressIPWatcher struct { + sync.Mutex + + localIP string + oc *ovsController + + networkClient networkclient.Interface + iptables *NodeIPTables + + // from HostSubnets + nodesByNodeIP map[string]*nodeEgress + nodesByEgressIP map[string]*nodeEgress + + // From NetNamespaces + namespacesByVNID map[uint32]*namespaceEgress + namespacesByEgressIP map[string]*namespaceEgress + + localEgressLink netlink.Link + localEgressIPMaskLen int + + testModeChan chan string +} + +func newEgressIPWatcher(localIP string, oc *ovsController) *egressIPWatcher { + return &egressIPWatcher{ + localIP: localIP, + oc: oc, + + nodesByNodeIP: make(map[string]*nodeEgress), + nodesByEgressIP: make(map[string]*nodeEgress), + + namespacesByVNID: make(map[uint32]*namespaceEgress), + namespacesByEgressIP: make(map[string]*namespaceEgress), + } +} + +func (eip *egressIPWatcher) Start(networkClient networkclient.Interface, iptables *NodeIPTables) error { + eip.iptables = iptables + eip.networkClient = networkClient + + go utilwait.Forever(eip.watchHostSubnets, 0) + go utilwait.Forever(eip.watchNetNamespaces, 0) + return nil +} + +func ipToHex(ip string) string { + bytes := net.ParseIP(ip) + if bytes == nil { + return "invalid IP: shouldn't happen" + } + bytes = bytes.To4() + return fmt.Sprintf("0x%02x%02x%02x%02x", bytes[0], bytes[1], bytes[2], bytes[3]) +} + +func (eip *egressIPWatcher) watchHostSubnets() { + common.RunEventQueue(eip.networkClient.Network().RESTClient(), common.HostSubnets, func(delta cache.Delta) error { + hs := delta.Object.(*networkapi.HostSubnet) + + var egressIPs []string + if delta.Type != cache.Deleted { + egressIPs = hs.EgressIPs + } + + eip.updateNode(hs.HostIP, egressIPs) + return nil + }) +} + +func (eip *egressIPWatcher) updateNode(nodeIP string, nodeEgressIPs []string) { + eip.Lock() + defer eip.Unlock() + + node := eip.nodesByNodeIP[nodeIP] + if node == nil { + if len(nodeEgressIPs) == 0 { + return + } + node = &nodeEgress{nodeIP: nodeIP, egressIPs: sets.NewString()} + eip.nodesByNodeIP[nodeIP] = node + } else if len(nodeEgressIPs) == 0 { + delete(eip.nodesByNodeIP, nodeIP) + } + oldEgressIPs := node.egressIPs + node.egressIPs = sets.NewString(nodeEgressIPs...) + + // Process new EgressIPs + for _, ip := range node.egressIPs.Difference(oldEgressIPs).UnsortedList() { + eip.nodesByEgressIP[ip] = node + hex := ipToHex(ip) + claimedNodeIP := nodeIP + + if nodeIP == eip.localIP { + if err := eip.claimEgressIP(ip, hex); err != nil { + glog.Errorf("Error claiming Egress IP %q: %v", ip, err) + claimedNodeIP = "" + } + } + + if ns, exists := eip.namespacesByEgressIP[ip]; exists { + if ns.assignedIP == "" { + ns.assignedIP = ip + ns.nodeIP = claimedNodeIP + err := eip.oc.UpdateNamespaceEgressRules(ns.vnid, claimedNodeIP, hex) + if err != nil { + glog.Errorf("Error updating Namespace egress rules: %v", err) + } + } + } + } + + // Process removed EgressIPs + for _, ip := range oldEgressIPs.Difference(node.egressIPs).UnsortedList() { + delete(eip.nodesByEgressIP, ip) + hex := ipToHex(ip) + + if nodeIP == eip.localIP { + if err := eip.releaseEgressIP(ip, hex); err != nil { + glog.Errorf("Error releasing Egress IP %q: %v", ip, err) + } + } + + if ns, exists := eip.namespacesByEgressIP[ip]; exists { + if ns.assignedIP == ip { + ns.assignedIP = "" + ns.nodeIP = "" + err := eip.oc.UpdateNamespaceEgressRules(ns.vnid, "", hex) + if err != nil { + glog.Errorf("Error updating Namespace egress rules: %v", err) + } + } + } + } +} + +func (eip *egressIPWatcher) watchNetNamespaces() { + common.RunEventQueue(eip.networkClient.Network().RESTClient(), common.NetNamespaces, func(delta cache.Delta) error { + netns := delta.Object.(*networkapi.NetNamespace) + + var egressIP string + if delta.Type != cache.Deleted && len(netns.EgressIPs) != 0 { + egressIP = netns.EgressIPs[0] + } + + eip.updateNamespace(netns.NetID, egressIP) + return nil + }) +} + +func (eip *egressIPWatcher) updateNamespace(vnid uint32, egressIP string) { + eip.Lock() + defer eip.Unlock() + + ns := eip.namespacesByVNID[vnid] + if ns == nil { + if egressIP == "" { + return + } + ns = &namespaceEgress{vnid: vnid} + eip.namespacesByVNID[vnid] = ns + } + if ns.claimedIP == egressIP { + return + } + + if ns.claimedIP != "" { + delete(eip.namespacesByEgressIP, ns.claimedIP) + ns.assignedIP = "" + ns.nodeIP = "" + } + ns.claimedIP = egressIP + eip.namespacesByEgressIP[egressIP] = ns + if node := eip.nodesByEgressIP[egressIP]; node != nil { + ns.assignedIP = egressIP + ns.nodeIP = node.nodeIP + } + + egressHex := "" + if egressIP != "" { + egressHex = ipToHex(egressIP) + } + + err := eip.oc.UpdateNamespaceEgressRules(ns.vnid, ns.nodeIP, egressHex) + if err != nil { + glog.Errorf("Error updating Namespace egress rules: %v", err) + } +} + +func (eip *egressIPWatcher) claimEgressIP(egressIP, egressHex string) error { + if eip.testModeChan != nil { + eip.testModeChan <- fmt.Sprintf("claim %s", egressIP) + return nil + } + + if eip.localEgressLink == nil { + links, err := netlink.LinkList() + if err != nil { + return fmt.Errorf("could not get list of network interfaces while adding egress IP: %v", err) + } + linkLoop: + for _, link := range links { + addrs, err := netlink.AddrList(link, syscall.AF_INET) + if err != nil { + return fmt.Errorf("could not get addresses of interface %q while adding egress IP: %v", link.Attrs().Name, err) + } + + for _, addr := range addrs { + if addr.IP.String() == eip.localIP { + eip.localEgressLink = link + eip.localEgressIPMaskLen, _ = addr.Mask.Size() + break linkLoop + } + } + } + + if eip.localEgressLink == nil { + return fmt.Errorf("could not find network interface with the address %q while adding egress IP", eip.localIP) + } + } + + egressIPNet := fmt.Sprintf("%s/%d", egressIP, eip.localEgressIPMaskLen) + addr, err := netlink.ParseAddr(egressIPNet) + if err != nil { + return fmt.Errorf("could not parse egress IP %q: %v", egressIPNet, err) + } + err = netlink.AddrAdd(eip.localEgressLink, addr) + if err != nil { + return fmt.Errorf("could not add egress IP %q to %s: %v", egressIPNet, eip.localEgressLink.Attrs().Name, err) + } + + if err := eip.iptables.AddEgressIPRules(egressIP, egressHex); err != nil { + return fmt.Errorf("could not add egress IP iptables rule: %v", err) + } + + return nil +} + +func (eip *egressIPWatcher) releaseEgressIP(egressIP, egressHex string) error { + if eip.testModeChan != nil { + eip.testModeChan <- fmt.Sprintf("release %s", egressIP) + return nil + } + + if eip.localEgressLink == nil { + return nil + } + + egressIPNet := fmt.Sprintf("%s/%d", egressIP, eip.localEgressIPMaskLen) + addr, err := netlink.ParseAddr(egressIPNet) + if err != nil { + return fmt.Errorf("could not parse egress IP %q: %v", egressIPNet, err) + } + err = netlink.AddrDel(eip.localEgressLink, addr) + if err != nil { + if err == syscall.EADDRNOTAVAIL { + glog.V(2).Infof("Could not delete egress IP %q from %s: no such address", egressIPNet, eip.localEgressLink.Attrs().Name) + } else { + return fmt.Errorf("could not delete egress IP %q from %s: %v", egressIPNet, eip.localEgressLink.Attrs().Name, err) + } + } + + if err := eip.iptables.DeleteEgressIPRules(egressIP, egressHex); err != nil { + return fmt.Errorf("could not delete egress IP iptables rule: %v", err) + } + + return nil +} diff --git a/pkg/network/node/egressip_test.go b/pkg/network/node/egressip_test.go new file mode 100644 index 000000000000..ae72d24b7e63 --- /dev/null +++ b/pkg/network/node/egressip_test.go @@ -0,0 +1,275 @@ +package node + +import ( + "fmt" + "testing" +) + +func assertNoNetlinkChanges(eip *egressIPWatcher) error { + select { + case change := <-eip.testModeChan: + return fmt.Errorf("Unexpected netlink change %q", change) + default: + return nil + } +} + +func assertNetlinkChange(eip *egressIPWatcher, expected string) error { + select { + case change := <-eip.testModeChan: + if change == expected { + return nil + } + return fmt.Errorf("Unexpected netlink change %q (expected %q)", change, expected) + default: + return fmt.Errorf("Missing netlink change (expected %q)", expected) + } +} + +func TestEgressIP(t *testing.T) { + ovsif, oc, origFlows := setupOVSController(t) + if oc.localIP != "172.17.0.4" { + panic("details of fake ovsController changed") + } + eip := newEgressIPWatcher("172.17.0.4", oc) + eip.testModeChan = make(chan string, 10) + + eip.updateNode("172.17.0.3", []string{}) + eip.updateNode("172.17.0.4", []string{}) + eip.updateNamespace(42, "") + eip.updateNamespace(43, "") + + // No namespaces use egress yet, so should be no changes + err := assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err := ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows) // no changes + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + + // Assign NetNamespace.EgressIP first, then HostSubnet.EgressIP, with a remote EgressIP + eip.updateNamespace(42, "172.17.0.100") + err = assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowAdded, + match: []string{"table=100", "reg0=42", "drop"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + + eip.updateNode("172.17.0.3", []string{"172.17.0.100"}) + err = assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowAdded, + match: []string{"table=100", "reg0=42", "172.17.0.3->tun_dst"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + origFlows = flows + + // Assign HostSubnet.EgressIP first, then NetNamespace.EgressIP, with a remote EgressIP + eip.updateNode("172.17.0.3", []string{"172.17.0.101", "172.17.0.100"}) + err = assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + + eip.updateNamespace(43, "172.17.0.101") + err = assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowAdded, + match: []string{"table=100", "reg0=43", "172.17.0.3->tun_dst"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + origFlows = flows + + // Assign NetNamespace.EgressIP first, then HostSubnet.EgressIP, with a local EgressIP + eip.updateNamespace(44, "172.17.0.102") + err = assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowAdded, + match: []string{"table=100", "reg0=44", "drop"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + + eip.updateNode("172.17.0.4", []string{"172.17.0.102"}) + err = assertNetlinkChange(eip, "claim 172.17.0.102") + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowAdded, + match: []string{"table=100", "reg0=44", "0xac110066->pkt_mark", "output:2"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + origFlows = flows + + // Assign HostSubnet.EgressIP first, then NetNamespace.EgressIP, with a local EgressIP + eip.updateNode("172.17.0.4", []string{"172.17.0.102", "172.17.0.103"}) + err = assertNetlinkChange(eip, "claim 172.17.0.103") + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + + eip.updateNamespace(45, "172.17.0.103") + err = assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowAdded, + match: []string{"table=100", "reg0=45", "0xac110067->pkt_mark", "output:2"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + origFlows = flows + + // Drop namespace EgressIP + eip.updateNamespace(44, "") + err = assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowRemoved, + match: []string{"table=100", "reg0=44", "0xac110066->pkt_mark", "output:2"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + origFlows = flows + + // Drop remote node EgressIP + eip.updateNode("172.17.0.3", []string{"172.17.0.100"}) + err = assertNoNetlinkChanges(eip) + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowRemoved, + match: []string{"table=100", "reg0=43", "172.17.0.3->tun_dst"}, + }, + flowChange{ + kind: flowAdded, + match: []string{"table=100", "reg0=43", "drop"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } + origFlows = flows + + // Drop local node EgressIP + eip.updateNode("172.17.0.4", []string{"172.17.0.102"}) + err = assertNetlinkChange(eip, "release 172.17.0.103") + if err != nil { + t.Fatalf("%v", err) + } + flows, err = ovsif.DumpFlows() + if err != nil { + t.Fatalf("Unexpected error dumping flows: %v", err) + } + err = assertFlowChanges(origFlows, flows, + flowChange{ + kind: flowRemoved, + match: []string{"table=100", "reg0=45", "0xac110067->pkt_mark", "output:2"}, + }, + flowChange{ + kind: flowAdded, + match: []string{"table=100", "reg0=45", "drop"}, + }, + ) + if err != nil { + t.Fatalf("Unexpected flow changes: %v", err) + } +} diff --git a/pkg/network/node/iptables.go b/pkg/network/node/iptables.go index 8a67a4586842..de00cf02796c 100644 --- a/pkg/network/node/iptables.go +++ b/pkg/network/node/iptables.go @@ -204,3 +204,24 @@ func (n *NodeIPTables) getNodeIPTablesChains() []Chain { }) return chainArray } + +func (n *NodeIPTables) AddEgressIPRules(egressIP, egressHex string) error { + for _, cidr := range n.clusterNetworkCIDR { + _, err := n.ipt.EnsureRule(iptables.Prepend, iptables.TableNAT, iptables.Chain("OPENSHIFT-MASQUERADE"), "-s", cidr, "-m", "mark", "--mark", egressHex, "-j", "SNAT", "--to-source", egressIP) + if err != nil { + return err + } + } + _, err := n.ipt.EnsureRule(iptables.Append, iptables.TableFilter, iptables.Chain("OPENSHIFT-FIREWALL-ALLOW"), "-d", egressIP, "-m", "conntrack", "--ctstate", "NEW", "-j", "REJECT") + return err +} + +func (n *NodeIPTables) DeleteEgressIPRules(egressIP, egressHex string) error { + for _, cidr := range n.clusterNetworkCIDR { + err := n.ipt.DeleteRule(iptables.TableNAT, iptables.Chain("OPENSHIFT-MASQUERADE"), "-s", cidr, "-m", "mark", "--mark", egressHex, "-j", "SNAT", "--to-source", egressIP) + if err != nil { + return err + } + } + return n.ipt.DeleteRule(iptables.TableFilter, iptables.Chain("OPENSHIFT-FIREWALL-ALLOW"), "-d", egressIP, "-m", "conntrack", "--ctstate", "NEW", "-j", "REJECT") +} diff --git a/pkg/network/node/node.go b/pkg/network/node/node.go index 138796c7ba89..0fde95df59d7 100644 --- a/pkg/network/node/node.go +++ b/pkg/network/node/node.go @@ -111,6 +111,8 @@ type OsdnNode struct { runtimeEndpoint string runtimeRequestTimeout time.Duration runtimeService kubeletapi.RuntimeService + + egressIP *egressIPWatcher } // Called by higher layers to create the plugin SDN node instance @@ -172,7 +174,7 @@ func New(c *OsdnNodeConfig) (network.NodeInterface, error) { if err != nil { return nil, err } - oc := NewOVSController(ovsif, pluginId, useConnTrack) + oc := NewOVSController(ovsif, pluginId, useConnTrack, c.SelfIP) plugin := &OsdnNode{ policy: policy, @@ -188,6 +190,7 @@ func New(c *OsdnNodeConfig) (network.NodeInterface, error) { egressPolicies: make(map[uint32][]networkapi.EgressNetworkPolicy), egressDNS: common.NewEgressDNS(), kubeInformers: c.KubeInformers, + egressIP: newEgressIPWatcher(c.SelfIP, oc), runtimeEndpoint: c.RuntimeEndpoint, // 2 minutes is the current default value used in kubelet @@ -332,7 +335,9 @@ func (node *OsdnNode) Start() error { if err != nil { return err } - + if err = node.egressIP.Start(node.networkClient, nodeIPTables); err != nil { + return err + } if err = node.policy.Start(node); err != nil { return err } diff --git a/pkg/network/node/ovscontroller.go b/pkg/network/node/ovscontroller.go index 06201ab4d930..2406489439ae 100644 --- a/pkg/network/node/ovscontroller.go +++ b/pkg/network/node/ovscontroller.go @@ -24,6 +24,7 @@ type ovsController struct { ovs ovs.Interface pluginId int useConnTrack bool + localIP string } const ( @@ -32,13 +33,13 @@ const ( Vxlan0 = "vxlan0" // rule versioning; increment each time flow rules change - ruleVersion = 4 + ruleVersion = 5 ruleVersionTable = 253 ) -func NewOVSController(ovsif ovs.Interface, pluginId int, useConnTrack bool) *ovsController { - return &ovsController{ovs: ovsif, pluginId: pluginId, useConnTrack: useConnTrack} +func NewOVSController(ovsif ovs.Interface, pluginId int, useConnTrack bool, localIP string) *ovsController { + return &ovsController{ovs: ovsif, pluginId: pluginId, useConnTrack: useConnTrack, localIP: localIP} } func (oc *ovsController) getVersionNote() string { @@ -63,7 +64,7 @@ func (oc *ovsController) AlreadySetUp() bool { return false } -func (oc *ovsController) SetupOVS(clusterNetworkCIDR []string, serviceNetworkCIDR, localSubnetCIDR, localSubnetGateway, nodeIP string) error { +func (oc *ovsController) SetupOVS(clusterNetworkCIDR []string, serviceNetworkCIDR, localSubnetCIDR, localSubnetGateway string) error { err := oc.ovs.AddBridge("fail-mode=secure", "protocols=OpenFlow13") if err != nil { return err @@ -188,11 +189,15 @@ func (oc *ovsController) SetupOVS(clusterNetworkCIDR []string, serviceNetworkCID // eg, "table=90, priority=100, ip, nw_dst=${remote_subnet_cidr}, actions=move:NXM_NX_REG0[]->NXM_NX_TUN_ID[0..31], set_field:${remote_node_ip}->tun_dst,output:1" otx.AddFlow("table=90, priority=0, actions=drop") - // Table 100: egress network policy dispatch; edited by UpdateEgressNetworkPolicy() - // eg, "table=100, reg0=${tenant_id}, priority=2, ip, nw_dst=${external_cidr}, actions=drop - otx.AddFlow("table=100, priority=0, actions=output:2") - otx.AddFlow("table=100, priority=%d,tcp,tcp_dst=53,nw_dst=%s,actions=output:2", networkapi.EgressNetworkPolicyMaxRules+1, nodeIP) - otx.AddFlow("table=100, priority=%d,udp,udp_dst=53,nw_dst=%s,actions=output:2", networkapi.EgressNetworkPolicyMaxRules+1, nodeIP) + // Table 100: egress routing; edited by UpdateNamespaceEgressRules() + // eg, "table=100, priority=100, reg0=${tenant_id}, ip, actions=set_field:${egress_ip_hex}->pkt_mark,output:2" + otx.AddFlow("table=100, priority=0, actions=goto_table:101") + + // Table 101: egress network policy dispatch; edited by UpdateEgressNetworkPolicy() + // eg, "table=101, reg0=${tenant_id}, priority=2, ip, nw_dst=${external_cidr}, actions=drop + otx.AddFlow("table=101, priority=%d,tcp,tcp_dst=53,nw_dst=%s,actions=output:2", networkapi.EgressNetworkPolicyMaxRules+1, oc.localIP) + otx.AddFlow("table=101, priority=%d,udp,udp_dst=53,nw_dst=%s,actions=output:2", networkapi.EgressNetworkPolicyMaxRules+1, oc.localIP) + otx.AddFlow("table=101, priority=0, actions=output:2") // Table 110: outbound multicast filtering, updated by UpdateLocalMulticastFlows() // eg, "table=110, priority=100, reg0=${tenant_id}, actions=goto_table:111 @@ -434,7 +439,7 @@ func (oc *ovsController) UpdateEgressNetworkPolicyRules(policies []networkapi.Eg var inputErr error if len(policies) == 0 { - otx.DeleteFlows("table=100, reg0=%d", vnid) + otx.DeleteFlows("table=101, reg0=%d", vnid) } else if vnid == 0 { inputErr = fmt.Errorf("EgressNetworkPolicy in global network namespace is not allowed (%s); ignoring", policyNames(policies)) } else if len(namespaces) > 1 { @@ -442,18 +447,18 @@ func (oc *ovsController) UpdateEgressNetworkPolicyRules(policies []networkapi.Eg // Even though Egress network policy is defined per namespace, its implementation is based on VNIDs. // So in case of shared network namespaces, egress policy of one namespace will affect all other namespaces that are sharing the network which might not be desirable. inputErr = fmt.Errorf("EgressNetworkPolicy not allowed in shared NetNamespace (%s); dropping all traffic", strings.Join(namespaces, ", ")) - otx.DeleteFlows("table=100, reg0=%d", vnid) - otx.AddFlow("table=100, reg0=%d, priority=1, actions=drop", vnid) + otx.DeleteFlows("table=101, reg0=%d", vnid) + otx.AddFlow("table=101, reg0=%d, priority=1, actions=drop", vnid) } else if len(policies) > 1 { // Rationale: If we have allowed more than one policy, we could end up with different network restrictions depending // on the order of policies that were processed and also it doesn't give more expressive power than a single policy. inputErr = fmt.Errorf("multiple EgressNetworkPolicies in same network namespace (%s) is not allowed; dropping all traffic", policyNames(policies)) - otx.DeleteFlows("table=100, reg0=%d", vnid) - otx.AddFlow("table=100, reg0=%d, priority=1, actions=drop", vnid) + otx.DeleteFlows("table=101, reg0=%d", vnid) + otx.AddFlow("table=101, reg0=%d, priority=1, actions=drop", vnid) } else /* vnid != 0 && len(policies) == 1 */ { // Temporarily drop all outgoing traffic, to avoid race conditions while modifying the other rules - otx.AddFlow("table=100, reg0=%d, cookie=1, priority=65535, actions=drop", vnid) - otx.DeleteFlows("table=100, reg0=%d, cookie=0/1", vnid) + otx.AddFlow("table=101, reg0=%d, cookie=1, priority=65535, actions=drop", vnid) + otx.DeleteFlows("table=101, reg0=%d, cookie=0/1", vnid) dnsFound := false for i, rule := range policies[0].Spec.Egress { @@ -488,18 +493,18 @@ func (oc *ovsController) UpdateEgressNetworkPolicyRules(policies []networkapi.Eg dst = fmt.Sprintf(", nw_dst=%s", selector) } - otx.AddFlow("table=100, reg0=%d, priority=%d, ip%s, actions=%s", vnid, priority, dst, action) + otx.AddFlow("table=101, reg0=%d, priority=%d, ip%s, actions=%s", vnid, priority, dst, action) } } if dnsFound { if err := common.CheckDNSResolver(); err != nil { inputErr = fmt.Errorf("DNS resolver failed: %v, dropping all traffic for namespace: %q", err, namespaces[0]) - otx.DeleteFlows("table=100, reg0=%d", vnid) - otx.AddFlow("table=100, reg0=%d, priority=1, actions=drop", vnid) + otx.DeleteFlows("table=101, reg0=%d", vnid) + otx.AddFlow("table=101, reg0=%d, priority=1, actions=drop", vnid) } } - otx.DeleteFlows("table=100, reg0=%d, cookie=1/1", vnid) + otx.DeleteFlows("table=101, reg0=%d, cookie=1/1", vnid) } txErr := otx.EndTransaction() @@ -677,3 +682,22 @@ func (oc *ovsController) FindUnusedVNIDs() []int { return policyVNIDs.Difference(inUseVNIDs).UnsortedList() } + +func (oc *ovsController) UpdateNamespaceEgressRules(vnid uint32, nodeIP, egressHex string) error { + otx := oc.ovs.NewTransaction() + otx.DeleteFlows("table=100, reg0=%d", vnid) + + if egressHex == "" { + // Namespace no longer has an EgressIP; no VNID-specific rules needed + } else if nodeIP == "" { + // Namespace has Egress IP, but it is unavailable, so drop egress traffic + otx.AddFlow("table=100, priority=100, reg0=%d, actions=drop", vnid) + } else if nodeIP == oc.localIP { + // Local Egress IP + otx.AddFlow("table=100, priority=100, reg0=%d, ip, actions=set_field:%s->pkt_mark,output:2", vnid, egressHex) + } else { + // Remote Egress IP; send via VXLAN + otx.AddFlow("table=100, priority=100, reg0=%d, ip, actions=move:NXM_NX_REG0[]->NXM_NX_TUN_ID[0..31],set_field:%s->tun_dst,output:1", vnid, nodeIP) + } + return otx.EndTransaction() +} diff --git a/pkg/network/node/ovscontroller_test.go b/pkg/network/node/ovscontroller_test.go index a8345ae66833..ee2ae4625cf9 100644 --- a/pkg/network/node/ovscontroller_test.go +++ b/pkg/network/node/ovscontroller_test.go @@ -16,10 +16,10 @@ import ( kapi "k8s.io/kubernetes/pkg/api" ) -func setup(t *testing.T) (ovs.Interface, *ovsController, []string) { +func setupOVSController(t *testing.T) (ovs.Interface, *ovsController, []string) { ovsif := ovs.NewFake(Br0) - oc := NewOVSController(ovsif, 0, true) - err := oc.SetupOVS([]string{"10.128.0.0/14"}, "172.30.0.0/16", "10.128.0.0/23", "10.128.0.1", "172.17.0.4") + oc := NewOVSController(ovsif, 0, true, "172.17.0.4") + err := oc.SetupOVS([]string{"10.128.0.0/14"}, "172.30.0.0/16", "10.128.0.0/23", "10.128.0.1") if err != nil { t.Fatalf("Unexpected error setting up OVS: %v", err) } @@ -100,7 +100,7 @@ func assertFlowChanges(origFlows, newFlows []string, changes ...flowChange) erro } func TestOVSHostSubnet(t *testing.T) { - ovsif, oc, origFlows := setup(t) + ovsif, oc, origFlows := setupOVSController(t) hs := networkapi.HostSubnet{ TypeMeta: metav1.TypeMeta{ @@ -156,7 +156,7 @@ func TestOVSHostSubnet(t *testing.T) { } func TestOVSService(t *testing.T) { - ovsif, oc, origFlows := setup(t) + ovsif, oc, origFlows := setupOVSController(t) svc := kapi.Service{ TypeMeta: metav1.TypeMeta{ @@ -223,7 +223,7 @@ const ( ) func TestOVSPod(t *testing.T) { - ovsif, oc, origFlows := setup(t) + ovsif, oc, origFlows := setupOVSController(t) // Add ofport, err := oc.SetUpPod("veth1", "10.128.0.2", "11:22:33:44:55:66", sandboxID, 42) @@ -423,7 +423,7 @@ func TestGetPodDetails(t *testing.T) { } func TestOVSMulticast(t *testing.T) { - ovsif, oc, origFlows := setup(t) + ovsif, oc, origFlows := setupOVSController(t) // local flows err := oc.UpdateLocalMulticastFlows(99, true, []int{4, 5, 6}) @@ -631,7 +631,7 @@ func assertENPFlowAdditions(origFlows, newFlows []string, additions ...enpFlowAd var change flowChange change.kind = flowAdded change.match = []string{ - "table=100", + "table=101", fmt.Sprintf("reg0=%d", addition.vnid), fmt.Sprintf("priority=%d", len(addition.policy.Spec.Egress)-i), } @@ -653,7 +653,7 @@ func assertENPFlowAdditions(origFlows, newFlows []string, additions ...enpFlowAd } func TestOVSEgressNetworkPolicy(t *testing.T) { - ovsif, oc, origFlows := setup(t) + ovsif, oc, origFlows := setupOVSController(t) // SUCCESSFUL CASES @@ -951,25 +951,21 @@ func TestOVSEgressNetworkPolicy(t *testing.T) { func TestAlreadySetUp(t *testing.T) { testcases := []struct { flow string - note string success bool }{ { // Good note - flow: "cookie=0x0, duration=4.796s, table=253, n_packets=0, n_bytes=0, actions=note:00.04.00.00.00.00", - note: "00.04", + flow: "cookie=0x0, duration=4.796s, table=253, n_packets=0, n_bytes=0, actions=note:00.05.00.00.00.00", success: true, }, { // Wrong table - flow: "cookie=0x0, duration=4.796s, table=10, n_packets=0, n_bytes=0, actions=note:00.04.00.00.00.00", - note: "00.04", + flow: "cookie=0x0, duration=4.796s, table=10, n_packets=0, n_bytes=0, actions=note:00.05.00.00.00.00", success: false, }, { // No note flow: "cookie=0x0, duration=4.796s, table=253, n_packets=0, n_bytes=0, actions=goto_table:50", - note: "00.04", success: false, }, } @@ -979,7 +975,7 @@ func TestAlreadySetUp(t *testing.T) { if err := ovsif.AddBridge("fail-mode=secure", "protocols=OpenFlow13"); err != nil { t.Fatalf("(%d) unexpected error from AddBridge: %v", i, err) } - oc := NewOVSController(ovsif, 0, true) + oc := NewOVSController(ovsif, 0, true, "172.17.0.4") otx := ovsif.NewTransaction() otx.AddFlow(tc.flow) @@ -1091,7 +1087,7 @@ func TestSyncVNIDRules(t *testing.T) { } for i, tc := range testcases { - _, oc, _ := setup(t) + _, oc, _ := setupOVSController(t) otx := oc.NewTransaction() for _, flow := range tc.flows { diff --git a/pkg/network/node/sdn_controller.go b/pkg/network/node/sdn_controller.go index 66e2beee3fd4..41b42e845233 100644 --- a/pkg/network/node/sdn_controller.go +++ b/pkg/network/node/sdn_controller.go @@ -173,7 +173,7 @@ func (plugin *OsdnNode) SetupSDN() (bool, error) { } glog.V(5).Infof("[SDN setup] full SDN setup required") - err = plugin.oc.SetupOVS(clusterNetworkCIDRs, serviceNetworkCIDR, localSubnetCIDR, localSubnetGateway, plugin.localIP) + err = plugin.oc.SetupOVS(clusterNetworkCIDRs, serviceNetworkCIDR, localSubnetCIDR, localSubnetGateway) if err != nil { return false, err } diff --git a/pkg/oc/cli/describe/describer.go b/pkg/oc/cli/describe/describer.go index ecf246545f79..6fc68fb227fd 100644 --- a/pkg/oc/cli/describe/describer.go +++ b/pkg/oc/cli/describe/describer.go @@ -1741,6 +1741,7 @@ func (d *HostSubnetDescriber) Describe(namespace, name string, settings kprinter formatString(out, "Node", hs.Host) formatString(out, "Node IP", hs.HostIP) formatString(out, "Pod Subnet", hs.Subnet) + formatString(out, "Egress IPs", strings.Join(hs.EgressIPs, ", ")) return nil }) } @@ -1759,6 +1760,7 @@ func (d *NetNamespaceDescriber) Describe(namespace, name string, settings kprint formatMeta(out, netns.ObjectMeta) formatString(out, "Name", netns.NetName) formatString(out, "ID", netns.NetID) + formatString(out, "Egress IPs", strings.Join(netns.EgressIPs, ", ")) return nil }) } diff --git a/pkg/oc/cli/describe/printer.go b/pkg/oc/cli/describe/printer.go index 34fed4489682..8aafddfbfb74 100644 --- a/pkg/oc/cli/describe/printer.go +++ b/pkg/oc/cli/describe/printer.go @@ -60,8 +60,8 @@ var ( // IsPersonalSubjectAccessReviewColumns contains known custom role extensions IsPersonalSubjectAccessReviewColumns = []string{"NAME"} - hostSubnetColumns = []string{"NAME", "HOST", "HOST IP", "SUBNET"} - netNamespaceColumns = []string{"NAME", "NETID"} + hostSubnetColumns = []string{"NAME", "HOST", "HOST IP", "SUBNET", "EGRESS IPS"} + netNamespaceColumns = []string{"NAME", "NETID", "EGRESS IPS"} clusterNetworkColumns = []string{"NAME", "CLUSTER NETWORKS", "SERVICE NETWORK", "PLUGIN NAME"} egressNetworkPolicyColumns = []string{"NAME"} @@ -1028,7 +1028,7 @@ func printGroupList(list *userapi.GroupList, w io.Writer, opts kprinters.PrintOp func printHostSubnet(h *networkapi.HostSubnet, w io.Writer, opts kprinters.PrintOptions) error { name := formatResourceName(opts.Kind, h.Name, opts.WithKind) - _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, h.Host, h.HostIP, h.Subnet) + _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t[%s]\n", name, h.Host, h.HostIP, h.Subnet, strings.Join(h.EgressIPs, ", ")) return err } @@ -1041,9 +1041,9 @@ func printHostSubnetList(list *networkapi.HostSubnetList, w io.Writer, opts kpri return nil } -func printNetNamespace(h *networkapi.NetNamespace, w io.Writer, opts kprinters.PrintOptions) error { - name := formatResourceName(opts.Kind, h.NetName, opts.WithKind) - _, err := fmt.Fprintf(w, "%s\t%d\n", name, h.NetID) +func printNetNamespace(n *networkapi.NetNamespace, w io.Writer, opts kprinters.PrintOptions) error { + name := formatResourceName(opts.Kind, n.NetName, opts.WithKind) + _, err := fmt.Fprintf(w, "%s\t%d\t[%s]\n", name, n.NetID, strings.Join(n.EgressIPs, ", ")) return err } diff --git a/pkg/openapi/zz_generated.openapi.go b/pkg/openapi/zz_generated.openapi.go index d8561dd4deac..a27c6bfeac68 100644 --- a/pkg/openapi/zz_generated.openapi.go +++ b/pkg/openapi/zz_generated.openapi.go @@ -7130,8 +7130,22 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope Format: "", }, }, + "egressIPs": { + SchemaProps: spec.SchemaProps{ + Description: "EgressIPs is the list of automatic egress IP addresses currently hosted by this node", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, }, - Required: []string{"host", "hostIP", "subnet"}, + Required: []string{"host", "hostIP", "subnet", "egressIPs"}, }, }, Dependencies: []string{ @@ -7221,6 +7235,20 @@ func GetOpenAPIDefinitions(ref openapi.ReferenceCallback) map[string]openapi.Ope Format: "int64", }, }, + "egressIPs": { + SchemaProps: spec.SchemaProps{ + Description: "EgressIPs is a list of reserved IPs that will be used as the source for external traffic coming from pods in this namespace. (If empty, external traffic will be masqueraded to Node IPs.)", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, }, Required: []string{"netname", "netid"}, },