Change:
Consider a service like The API refuses to accept this service, stating It works fine without . It works when using different ports. The iptables rules installed (in mode) are TCP/UDP specific. I'm not sure if this is deliberate but not well documented, or a bug? I don't see a reason not to allow a UDP and TCP node port to have the same number.
For context, I am trying to make the cluster DNS available to legacy systems with a bit of forwarding, and DNS really really wants to have UDP and TCP on the same port number.
what is your kubernetes version?
This is on 1.1.4. From a quick reading of the code this hasn't changed though.
This was intentionally omitted due to laziness and complexity. We hoped nobody would need it, since NodePorts are generally allocated automatically. It's mostly going to be a matter of adding a switch to look at 2 different saved allocation maps and figuring out how to transition. , Matthias Rampke wrote:
I've also hit this issue (see: ) when trying to expose DNS to queries from outside the cluster. Note that in my setup, the "NodePort" is different for UDP and TCP but only UDP appears to work (it's the first to be defined?).
I also run into this issue. I've got a service that wants to expose TCP and UDP port with the same number. Any plans to include this issue in any roadmap?
Help wanted. We're buried in high-priority bugs and features. I ACK that this behaves imperfectly, but I and my team will not have time to look at it in the immediate future.
FYI
I think the root cause is , when serviceport allocator attempts to reserve the provided port, it just use the port number not consider the port protocol. This is a bug of service port allocator I believe.
"bug", but yes. We'd need to store parallel bitmaps per protocol. Even then, it's not obvious when to use the same port number in different protocols, and whether that is REQUIRED or not. If you have a Service with port 53 in TCP and UDP, and I can't find any port number that is available in both protocols, should I: a) fail and reject the REST operation b) choose a different port number c) retry forever and hang the REST operation d) accept the REST operation but log an event saying I failed to do my job NodePort was really made for load-balancers, where the port number doesn't matter. Humans using it is "unexpected". , TonyAdo wrote:
this is what I'd expect. Port exhaustion is not really a temporary Matthias Rampke Engineer SoundCloud Ltd. +49 173 Managing Director: Alexander Ljung | Incorporated in England & Wales with Company No. HRB 110657B
I am afraid PR will not fix this issue. We have to store parallel bitmaps per protocol. I will open a new PR to fix this issue.
This alone is insufficient, but we might get away without storing two maps by simply saying the port map is unified across protocols if a service has 2 ports that use the same port number but different protocols, allocate the first nodePort and use the same value for the 2nd port This will prevent the case of ports being used in one protocol but not the other and failing to allocate. Something like: I haven;t tried too hard to break it, but something like that MIGHT work.
Additionally, if you have to store multiple bitmaps, you have a serious hassle on your hands for upgrading cluster in-place. Better to avoid it if you can.
sure, I will think about this more.
Hi, I have created and deployed a service similar to below in minikube environment.. however I am not to connect to the ports sometimes TCP/UDP ` apiVersion: v1 kind: Service metadata: name: dns spec: selector: … clusterIP: … type: NodePort ports: name: grpc protocol: TCP port: targetPort: grpc-port name: port-udp port: targetPort: 5062 protocol: UDP nodePort: name: port-tcp port: targetPort: 5062 protocol: TCP nodePort: Example: $kubectl get all service/-service NodePort 10.106.159.94 <none50051:/TCP,30:/UDP,:/TCP $ telnet 192.168.99.100 telnet: can't connect to remote host (192.168.99.100): Connection refused $ telnet localhost telnet: can't connect to remote host (127.0.0.1): Connection refused $nc -z -v -u 192.168.99.100 Connection to 192.168.99.100 port [udp/] succeeded! $ nc -z -v 192.168.99.100 nc: connect to 192.168.99.100 port (tcp) failed: Connection refused $ kubectl version Client Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.1", GitCommit:"", GitTreeState:"clean", BuildDate:"2019-10-02T17:01:15Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"linux/amd64"} Server Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.0", GitCommit:"", GitTreeState:"clean", BuildDate:"2019-09-18T14:27:17Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"} $netstat -an tcp 0 0 ::: ::: LISTEN udp 0 0 ::: :::*
!

Discussion:
} assignNodePorts := shouldAssignNodePorts(service) <ins> svcPortToNodePort := map[int]int{} </ins> for i := range service.Spec.Ports { servicePort := &service.Spec.Ports[i] <del> if servicePort.NodePort != 0 { err := nodePortOp.Allocate(int(servicePort.NodePort)) if err != nil { // TODO: when validation becomes versioned, this gets more complicated. el := field.ErrorList{field.Invalid(field.NewPath("spec", "ports").Index(i).Child("nodePort"), servicePort.NodePort, err.Error())} return nil, errors.NewInvalid(api.Kind("Service"), service.Name, el) </del> <ins> allocatedNodePort := svcPortToNodePort[int(servicePort.Port)] if allocatedNodePort == 0 { // This will only scan forward in the service.Spec.Ports list because any matches // before the current port would have been found in svcPortToNodePort. This is really // looking for any user provided values. np := findRequestedNodePort(int(servicePort.Port), service.Spec.Ports) if np != 0 { err := nodePortOp.Allocate(np) if err != nil { // TODO: when validation becomes versioned, this gets more complicated. el := field.ErrorList{field.Invalid(field.NewPath("spec", "ports").Index(i).Child("nodePort"), np, err.Error())} return nil, errors.NewInvalid(api.Kind("Service"), service.Name, el) } servicePort.NodePort = int32(np) svcPortToNodePort[int(servicePort.Port)] = np } else if assignNodePorts { nodePort, err := nodePortOp.AllocateNext() if err != nil { // TODO: what error should be returned here?  It's not a // field-level validation failure (the field is valid), and it's // not really an internal error. return nil, errors.NewInternalError(fmt.Errorf("failed to allocate a nodePort: %v", err)) } servicePort.NodePort = int32(nodePort) svcPortToNodePort[int(servicePort.Port)] = nodePort </ins> } <del> } else if assignNodePorts { nodePort, err := nodePortOp.AllocateNext() if err != nil { // TODO: what error should be returned here?  It's not a // field-level validation failure (the field is valid), and it's // not really an internal error. return nil, errors.NewInternalError(fmt.Errorf("failed to allocate a nodePort: %v", err)) </del> <ins> } else if int(servicePort.NodePort) != allocatedNodePort { if servicePort.NodePort == 0 { servicePort.NodePort = int32(allocatedNodePort) } else { err := nodePortOp.Allocate(int(servicePort.NodePort)) if err != nil { // TODO: when validation becomes versioned, this gets more complicated. el := field.ErrorList{field.Invalid(field.NewPath("spec", "ports").Index(i).Child("nodePort"), servicePort.NodePort, err.Error())} return nil, errors.NewInvalid(api.Kind("Service"), service.Name, el) } </ins> } <del> servicePort.NodePort = int32(nodePort) </del> } }

glog.V(4).Infof("Service type: %v does not need health check node port", service.Spec.Type) return false } <ins> // Loop through the service ports list, find one with the same port number and // NodePort specified, return this NodePort otherwise return 0. func findRequestedNodePort(port int, servicePorts []api.ServicePort) int { for i := range servicePorts { servicePort := servicePorts[i] if port == int(servicePort.Port) && servicePort.NodePort != 0 { return int(servicePort.NodePort) } } return 0 } </ins>

import ( "net" <ins> "reflect" </ins> "strings" "testing"

} } <ins> func TestServiceRegistryCreateMultiNodePortsService(t *testing.T) { storage, registry := NewTestREST(t, nil) testCases := []struct { svc             *api.Service name            string expectNodePorts []int }{ { svc: &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo1"}, Spec: api.ServiceSpec{ Selector:        map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, Type:            api.ServiceTypeNodePort, Ports: []api.ServicePort{ { Name:       "port-tcp", Port:       53, NodePort:   30053, TargetPort: intstr.FromInt(6503), Protocol:   api.ProtocolTCP, }, { Name:       "port-udp", Port:       53, NodePort:   30053, TargetPort: intstr.FromInt(6503), Protocol:   api.ProtocolUDP, }, }, }, }, name:            "foo1", expectNodePorts: []int{30053, 30053}, }, { svc: &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo2"}, Spec: api.ServiceSpec{ Selector:        map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, Type:            api.ServiceTypeNodePort, Ports: []api.ServicePort{ { Name:       "port-tcp", Port:       54, TargetPort: intstr.FromInt(6504), Protocol:   api.ProtocolTCP, }, { Name:       "port-udp", Port:       54, NodePort:   30054, TargetPort: intstr.FromInt(6504), Protocol:   api.ProtocolUDP, }, }, }, }, name:            "foo2", expectNodePorts: []int{30054, 30054}, }, { svc: &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo3"}, Spec: api.ServiceSpec{ Selector:        map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, Type:            api.ServiceTypeNodePort, Ports: []api.ServicePort{ { Name:       "port-tcp", Port:       55, NodePort:   30055, TargetPort: intstr.FromInt(6505), Protocol:   api.ProtocolTCP, }, { Name:       "port-udp", Port:       55, NodePort:   30056, TargetPort: intstr.FromInt(6506), Protocol:   api.ProtocolUDP, }, }, }, }, name:            "foo3", expectNodePorts: []int{30055, 30056}, }, } ctx := api.NewDefaultContext() for _, test := range testCases { created_svc, err := storage.Create(ctx, test.svc) if err != nil { t.Fatalf("Unexpected error: %v", err) } created_service := created_svc.(*api.Service) if !api.HasObjectMetaSystemFieldValues(&created_service.ObjectMeta) { t.Errorf("storage did not populate object meta field values") } if created_service.Name != test.name { t.Errorf("Expected %s, but got %s", test.name, created_service.Name) } serviceNodePorts := CollectServiceNodePorts(created_service) if !reflect.DeepEqual(serviceNodePorts, test.expectNodePorts) { t.Errorf("Expected %v, but got %v", test.expectNodePorts, serviceNodePorts) } srv, err := registry.GetService(ctx, test.name) if err != nil { t.Errorf("unexpected error: %v", err) } if srv == nil { t.Errorf("Failed to find service: %s", test.name) } } } </ins> func TestServiceStorageValidatesCreate(t *testing.T) { storage, _ := NewTestREST(t, nil) failureCases := map[string]api.Service{

} }) <ins> It("should use same NodePort with same port but different protocols", func() { serviceName := "nodeports" ns := f.Namespace.Name t := NewServerTest(c, ns, serviceName) defer func() { defer GinkgoRecover() errs := t.Cleanup() if len(errs) != 0 { framework.Failf("errors in cleanup: %v", errs) } }() By("creating service " + serviceName + " with same NodePort but different protocols in namespace " + ns) service := &api.Service{ ObjectMeta: api.ObjectMeta{ Name:      t.ServiceName, Namespace: t.Namespace, }, Spec: api.ServiceSpec{ Selector: t.Labels, Type:     api.ServiceTypeNodePort, Ports: []api.ServicePort{ { Name:     "tcp-port", Port:     53, Protocol: api.ProtocolTCP, }, { Name:     "udp-port", Port:     53, Protocol: api.ProtocolUDP, }, }, }, } result, err := t.CreateService(service) Expect(err).NotTo(HaveOccurred()) if len(result.Spec.Ports) != 2 { framework.Failf("got unexpected len(Spec.Ports) for new service: %v", result) } if result.Spec.Ports[0].NodePort != result.Spec.Ports[1].NodePort { framework.Failf("should use same NodePort for new service: %v", result) } }) </ins> It("should prevent NodePort collisions", func() { // TODO: use the ServiceTestJig here baseName := "nodeport-collision-"