External access to services in cluster on VPS nodes with public IPs

Hello everyone!
Need some help with my k8s deployment.
Maybe, I’m doing something totally wrong so, any advice regarding my whole setup also appreciated.

Cluster information:

Kubernetes version: v1.30.1
Cloud being used: bare-metal on VPS with public IPs
Installation method: kubeadm + Ansible
Host OS: Rocky Linux 9.4
CNI and version: flannel v0.25.1
CRI and version: containerd 1.7.16
Routing mode: IPVS

What I’m not using nor planning to:

  • Cloud-based k8s provider (GKE, AWS, …)
  • Cloud-based load balancer

I have 3 nodes (1 master and 2 slaves) with A DNS records at my provider’s zone as follows:

pub.name		x.x.x.x		(domain and almost every other subdomain for concrete service points to a master node's public IP)
mn.pub.name		x.x.x.x		(master)
sn1.pub.name		y.y.y.y		(slave 1)
sn2.pub.name		z.z.z.z		(slave 2)

Where pub.name - is my publicly available registered domain.
And x.x.x.x, y.y.y.y, z.z.z.z are public static IPs of respecting nodes. These IPs are not from the same subnet. Just some random public IPs assigned by provider on VPS creation.

I’m using IPv4 single stack; IPv6 is disabled on every node
Firewall is disabled. SELinux is disabled.

My goal - is to have a scalable k8s cluster based on VPS VMs with public IPs.
This cluster will be used for proof of concept purposes, so no production-grade requirements have to be met.

I want to have all the services to be available on a single IP address via corresponding DNS records.
My thought is that this single node handling all incoming requests should be master node. (Please, tell me if it’s better done otherwise)

I do not care for extra hop required to get from master node to a slave node hosting actual pod (POC).

Unfortunately for me, list of ports that supposed to be available on this public node goes beyond standart 80 & 443.
I would also need mail-related ports (25, 465, 587, …) for docker-mailserver installation
And a few others from system range (within 1000). Also, I definetely need ports 80 & 443 to be able to serve HTTP and HTTPS ingress traffic.

What I understand from documentation (correct me if I’m wrong here) - is that to have system ports publicly available, I have to use load balancer.
Across load balancers, MetalLB seemed to suite my use-case the best.
To use MetalLB, I have to deploy my k8s cluster in IPVS mode.

The problem is: once cluster works in IPVS mode, whenever I deploy any service with external IP, the node that is hosting pod corresponding to that service, goes into “NotReady” state. And ping from that node to master node fails.
It doesn’t matter if I’m using MetalLB to assign external IP to a service, or just configure externalIP in values.yaml for Helm installation of that service.

Here are some configs:
IPAddressPool resource definition for MetalLB:

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: ipap-public
  namespace: lb
spec:
  addresses:
    - 'x.x.x.x/32'

L2Adverdisement resource definition for MetalLB:

apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2a-public
  namespace: lb
spec:
  ipAddressPools:
    - ipap-public

Pods after installing docker-mailserver (All pods were in “Ready” status before installation):

kubectl get po -A -o wide
NAMESPACE      NAME                                               READY   STATUS        RESTARTS          AGE     IP               NODE           NOMINATED NODE   READINESS GATES
cert           manager-cert-manager-cainjector-5996484b89-2jvw9   1/1     Terminating   2 (3h40m ago)     46h     10.244.4.38      sn2.pub.name   <none>           <none>
cert           manager-cert-manager-cainjector-5996484b89-xsqxw   0/1     Pending       0                 3h11m   <none>           <none>         <none>           <none>
cert           manager-cert-manager-controller-77b697fdc8-8kj9l   0/1     Pending       0                 3h11m   <none>           <none>         <none>           <none>
cert           manager-cert-manager-controller-77b697fdc8-s7pcx   1/1     Terminating   2 (3h40m ago)     46h     10.244.4.34      sn2.pub.name   <none>           <none>
cert           manager-cert-manager-webhook-b4d6fb4db-6fst7       0/1     Pending       0                 3h11m   <none>           <none>         <none>           <none>
cert           manager-cert-manager-webhook-b4d6fb4db-xhlhm       1/1     Terminating   2 (3h40m ago)     46h     10.244.4.35      sn2.pub.name   <none>           <none>
ingress        nginx-ingress-nginx-controller-548c97b899-fd5rc    1/1     Terminating   2 (3h40m ago)     46h     10.244.4.37      sn2.pub.name   <none>           <none>
ingress        nginx-ingress-nginx-controller-548c97b899-h759b    0/1     Pending       0                 3h11m   <none>           <none>         <none>           <none>
kube-flannel   kube-flannel-ds-h6c7p                              1/1     Running       7 (3h40m ago)     8d      x.x.x.x          mn.pub.name    <none>           <none>
kube-flannel   kube-flannel-ds-pf2tn                              1/1     Running       120 (3h39m ago)   8d      z.z.z.z          sn2.pub.name   <none>           <none>
kube-flannel   kube-flannel-ds-wqlwm                              1/1     Running       248 (3h30m ago)   8d      y.y.y.y          sn1.pub.name   <none>           <none>
kube-system    coredns-7db6d8ff4d-bs8sl                           1/1     Running       7 (3h40m ago)     8d      10.244.0.17      mn.pub.name    <none>           <none>
kube-system    coredns-7db6d8ff4d-cdczf                           1/1     Running       7 (3h40m ago)     8d      10.244.0.16      mn.pub.name    <none>           <none>
kube-system    etcd-mn.pub.name                                   1/1     Running       7 (3h40m ago)     8d      x.x.x.x          mn.pub.name    <none>           <none>
kube-system    kube-apiserver-mn.pub.name                         1/1     Running       7 (3h40m ago)     8d      x.x.x.x          mn.pub.name    <none>           <none>
kube-system    kube-controller-manager-mn.pub.name                1/1     Running       7 (3h40m ago)     8d      x.x.x.x          mn.pub.name    <none>           <none>
kube-system    kube-proxy-9gghz                                   1/1     Running       9 (3h40m ago)     8d      y.y.y.y          sn1.pub.name   <none>           <none>
kube-system    kube-proxy-jx765                                   1/1     Running       7 (3h40m ago)     8d      x.x.x.x          mn.pub.name    <none>           <none>
kube-system    kube-proxy-rzxhl                                   1/1     Running       8 (3h40m ago)     8d      z.z.z.z          sn2.pub.name   <none>           <none>
kube-system    kube-scheduler-mn.pub.name                         1/1     Running       7 (3h40m ago)     8d      x.x.x.x          mn.pub.name    <none>           <none>
lb             metal-metallb-controller-586d8ff949-fscs5          0/1     Pending       0                 3h11m   <none>           <none>         <none>           <none>
lb             metal-metallb-speaker-js29q                        4/4     Running       12 (3h40m ago)    3d4h    x.x.x.x          mn.pub.name    <none>           <none>
lb             metal-metallb-speaker-rchjk                        4/4     Running       211 (3h31m ago)   3d4h    y.y.y.y          sn1.pub.name   <none>           <none>
lb             metal-metallb-speaker-wwzsh                        4/4     Running       16 (3h40m ago)    3d4h    z.z.z.z          sn2.pub.name   <none>           <none>
mail           server-docker-mailserver-dc6c85597-5bl8k           0/1     Pending       0                 3h11m   <none>           <none>         <none>           <none>
mail           server-docker-mailserver-dc6c85597-wq4qb           0/1     Terminating   0                 3h17m   <none>           sn1.pub.name   <none>           <none>

Nodes after installing docker-mailserver (nodes were all ready before installation):

kubectl get nodes -o wide
NAME           STATUS     ROLES           AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                      KERNEL-VERSION                 CONTAINER-RUNTIME
mn.pub.name    Ready      control-plane   8d    v1.30.1   x.x.x.x          <none>        Rocky Linux 9.4 (Blue Onyx)   5.14.0-427.16.1.el9_4.x86_64   containerd://1.7.16
sn1.pub.name   NotReady   <none>          8d    v1.30.1   y.y.y.y          <none>        Rocky Linux 9.4 (Blue Onyx)   5.14.0-427.16.1.el9_4.x86_64   containerd://1.7.16
sn2.pub.name   NotReady   <none>          8d    v1.30.1   z.z.z.z          <none>        Rocky Linux 9.4 (Blue Onyx)   5.14.0-427.16.1.el9_4.x86_64   containerd://1.7.16

Services after installing docker-mailserver:

kubectl get svc -A
NAMESPACE     NAME                                       TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)
AGE
cert          manager-cert-manager-controller-metrics    ClusterIP      10.96.2.46     <none>         9402/TCP
45h
cert          manager-cert-manager-webhook               ClusterIP      10.96.9.119    <none>         443/TCP
45h
default       kubernetes                                 ClusterIP      10.96.0.1      <none>         443/TCP
8d
ingress       nginx-ingress-nginx-controller             ClusterIP      10.96.8.89     <none>         80/TCP,443/TCP
2d
ingress       nginx-ingress-nginx-controller-admission   ClusterIP      10.96.15.151   <none>         443/TCP
2d
kube-system   kube-dns                                   ClusterIP      10.96.0.10     <none>         53/UDP,53/TCP,9153/TCP
8d
lb            metallb-webhook-service                    ClusterIP      10.96.4.83     <none>         443/TCP
3d3h
mail          server-docker-mailserver                   LoadBalancer   10.96.7.14     x.x.x.x   25:32646/TCP,465:32674/TCP,587:31297/TCP,10465:32246/TCP,10587:32643/TCP,143:31931/TCP,993:30564/TCP,10143:32139/TCP,10993:30131/TCP,11334:30402/TCP   108m

Ping & arping result from slave node 1 to master after installation:

[root@sn1 ~]# ping -c 4 mn.pub.name
PING mn.pub.name (x.x.x.x) 56(84) bytes of data.
From mn.pub.name (x.x.x.x) icmp_seq=1 Destination Port Unreachable
From mn.pub.name (x.x.x.x) icmp_seq=2 Destination Port Unreachable
From mn.pub.name (x.x.x.x) icmp_seq=3 Destination Port Unreachable
From mn.pub.name (x.x.x.x) icmp_seq=4 Destination Port Unreachable

--- mn.pub.name ping statistics ---
4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3004ms

[root@sn1 ~]# arping -c 4 -I eth0 x.x.x.x
ARPING x.x.x.x from y.y.y.y eth0
Unicast reply from x.x.x.x [FE:54:00:90:C0:0A]  44.181ms
Unicast reply from x.x.x.x [FE:54:00:90:C0:0A]  0.759ms
Unicast reply from x.x.x.x [FE:54:00:90:C0:0A]  0.679ms
Unicast reply from x.x.x.x [FE:54:00:90:C0:0A]  0.846ms
Sent 4 probes (1 broadcast(s))
Received 4 response(s)

IPVS routes:

ipvsadm -l
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  mn.pub.name:smtp rr
TCP  mn.pub.name:imap rr
TCP  mn.pub.name:urd rr
TCP  mn.pub.name:submission rr
TCP  mn.pub.name:imaps rr
TCP  mn.pub.name:10143 rr
TCP  mn.pub.name:10465 rr
TCP  mn.pub.name:10587 rr
TCP  mn.pub.name:10993 rr
TCP  mn.pub.name:11334 rr
TCP  mn.pub.name:30131 rr
TCP  mn.pub.name:30402 rr
TCP  mn.pub.name:30564 rr
TCP  mn.pub.name:31297 rr
TCP  mn.pub.name:31931 rr
TCP  mn.pub.name:32139 rr
TCP  mn.pub.name:32246 rr
TCP  mn.pub.name:32643 rr
TCP  mn.pub.name:32646 rr
TCP  mn.pub.name:32674 rr
TCP  mn.pub.name:https rr
-> mn.pub.name:sun-sr-https     Masq    1      4          0
TCP  mn.pub.name:domain rr
-> 10.244.0.16:domain           Masq    1      0          0
-> 10.244.0.17:domain           Masq    1      0          0
TCP  mn.pub.name:9153 rr
-> 10.244.0.16:9153             Masq    1      0          0
-> 10.244.0.17:9153             Masq    1      0          0
TCP  mn.pub.name:sec-pc2fax-srv rr
TCP  mn.pub.name:https rr
TCP  mn.pub.name:smtp rr
TCP  mn.pub.name:imap rr
TCP  mn.pub.name:urd rr
TCP  mn.pub.name:submission rr
TCP  mn.pub.name:imaps rr
TCP  mn.pub.name:10143 rr
TCP  mn.pub.name:10465 rr
TCP  mn.pub.name:10587 rr
TCP  mn.pub.name:10993 rr
TCP  mn.pub.name:11334 rr
TCP  mn.pub.name:http rr
TCP  mn.pub.name:https rr
TCP  mn.pub.name:https rr
TCP  mn.pub.name:https rr
TCP  mn.pub.name:30131 rr
TCP  mn.pub.name:30402 rr
TCP  mn.pub.name:30564 rr
TCP  mn.pub.name:31297 rr
TCP  mn.pub.name:31931 rr
TCP  mn.pub.name:32139 rr
TCP  mn.pub.name:32246 rr
TCP  mn.pub.name:32643 rr
TCP  mn.pub.name:32646 rr
TCP  mn.pub.name:32674 rr
TCP  mn.pub.name:30131 rr
TCP  mn.pub.name:30402 rr
TCP  mn.pub.name:30564 rr
TCP  mn.pub.name:31297 rr
TCP  mn.pub.name:31931 rr
TCP  mn.pub.name:32139 rr
TCP  mn.pub.name:32246 rr
TCP  mn.pub.name:32643 rr
TCP  mn.pub.name:32646 rr
TCP  mn.pub.name:32674 rr
UDP  mn.pub.name:domain rr
-> 10.244.0.16:domain           Masq    1      0          0
-> 10.244.0.17:domain           Masq    1      0          0

As soon as I’m uninstalling docker-mailserver, things got back to normal (nodes, pods, ping, …)

This page says I have to provide some IP addresses to MetalLB IPAddressPool other than public IPs assigned by my VPS provider. Like it is done in IPAddressPool resource manifest where spec.addresses is set to ['203.0.113.10-203.0.113.15']
But, provider’s support refuses to give me more than one IP per node.

Tried to post to Slack channels metallb and #ingress-nginx-users
No solution so far.

Any advices, suggestions?