|
| 1 | +# API |
| 2 | + |
| 3 | +Group `kuport.wlz.li`, version `v1alpha1`. Two kinds: a class the admin writes |
| 4 | +and a mapping the workload writes. |
| 5 | + |
| 6 | +## PortMapClass |
| 7 | + |
| 8 | +Cluster-scoped. The admin writes it. It says which nodes accept traffic, on |
| 9 | +which interfaces, which ports may be asked for, and which namespaces may ask. |
| 10 | + |
| 11 | +```yaml |
| 12 | +apiVersion: kuport.wlz.li/v1alpha1 |
| 13 | +kind: PortMapClass |
| 14 | +metadata: |
| 15 | + name: public |
| 16 | +spec: |
| 17 | + nodes: |
| 18 | + worker-1: |
| 19 | + interfaces: |
| 20 | + - wt0 |
| 21 | + - enp1s0 |
| 22 | + worker-2: |
| 23 | + interfaces: |
| 24 | + - wt0 |
| 25 | + - eth0 |
| 26 | + ports: |
| 27 | + min: 1024 |
| 28 | + max: 65535 |
| 29 | + reserved: [80, 443] |
| 30 | + namespaceSelector: |
| 31 | + matchLabels: |
| 32 | + kuport.wlz.li/public: allowed |
| 33 | + returnPath: |
| 34 | + mode: Vxlan |
| 35 | + vxlan: |
| 36 | + vni: 4242 |
| 37 | + port: 4790 |
| 38 | + subnet: 169.254.77.0/24 |
| 39 | +``` |
| 40 | +
|
| 41 | +| Field | Type | Meaning | |
| 42 | +| --- | --- | --- | |
| 43 | +| nodes | map of node name to object | Nodes that may accept traffic for this class: its accepting nodes. A name with no Node object in the cluster is skipped. Required, at least one entry. | |
| 44 | +| nodes.<name>.interfaces | list of string | Interface names that node's DNAT rules match on. One rule per interface. Naming them per node lets one class cover nodes whose NICs are named differently. Required, at least one. | |
| 45 | +| servingMode | enum | `Single` or `Multi`: how many accepting nodes program a mapping. Default `Single`. See [serving modes](datapath.md#serving-modes). | |
| 46 | +| ports.min, ports.max | int | The range a PortMap may ask for. Defaults 1, 65535. | |
| 47 | +| ports.reserved | list of int | Ports the admin keeps back. A PortMap naming one is rejected in status. | |
| 48 | +| namespaceSelector | label selector | Which namespaces may reference this class. Empty selects every namespace. | |
| 49 | +| returnPath.mode | enum | `Vxlan` or `None`. `None` refuses any mapping that would need a return link. | |
| 50 | +| returnPath.vxlan.vni | int | Base VXLAN network identifier for the links this class builds. Each slot takes the base plus its own number, because the kernel keys a device by VNI and port. | |
| 51 | +| returnPath.vxlan.port | int | UDP port for the links. Must differ from the CNI's, which is 8472 for Cilium. Default 4790. | |
| 52 | +| returnPath.vxlan.subnet | CIDR | Link addresses are allocated from here, a /31 per node pair (RFC 3021 point-to-point), so the default gives 128 slots. The slot for a pair is a recorded claim in status.links rather than a computed hash. Default 169.254.77.0/24. | |
| 53 | + |
| 54 | +Two classes whose accepting nodes overlap need their own `vni` and `subnet`. A |
| 55 | +class numbers its slots from zero, so two classes sharing a node would both ask |
| 56 | +for the base VNI and the first /31 there, and the kernel refuses the second of |
| 57 | +each. |
| 58 | + |
| 59 | +## PortMap |
| 60 | + |
| 61 | +Namespaced. The workload writes it, beside its Service. |
| 62 | + |
| 63 | +```yaml |
| 64 | +apiVersion: kuport.wlz.li/v1alpha1 |
| 65 | +kind: PortMap |
| 66 | +metadata: |
| 67 | + name: gameserver |
| 68 | + namespace: games |
| 69 | +spec: |
| 70 | + className: public |
| 71 | + protocol: UDP |
| 72 | + port: 3000 |
| 73 | + serviceRef: |
| 74 | + name: gameserver |
| 75 | + port: game |
| 76 | +``` |
| 77 | + |
| 78 | +| Field | Type | Meaning | |
| 79 | +| --- | --- | --- | |
| 80 | +| className | string | The PortMapClass this asks for. Required, immutable. | |
| 81 | +| protocol | enum | TCP or UDP. Required, immutable. | |
| 82 | +| port | int | The port clients dial on an accepting node, and the port the pod receives on. With endPort set, the first port of the range. Required, immutable. | |
| 83 | +| endPort | int | Last port of an inclusive range starting at port. Omit for a single port. Immutable together with port; the API rejects endPort below port. One nftables `dport <first>-<last>` match covers a whole range. | |
| 84 | +| interfaces | list of string | The interfaces this mapping binds on, narrowing what the class gives each node. Empty selects every interface the class gives the node. Mutable. | |
| 85 | +| serviceRef.name | string | A Service in the same namespace. Required. | |
| 86 | +| serviceRef.port | string | The named port on that Service. Required. | |
| 87 | + |
| 88 | +The Service may be any type, ClusterIP included. It exists so the agent has |
| 89 | +endpoints to follow and a named port to resolve. kuport never touches it. |
| 90 | + |
| 91 | +The immutable fields are immutable because changing them is indistinguishable |
| 92 | +from deleting one mapping and creating another, and the reconcile is simpler if |
| 93 | +it never has to unwind a half-changed mapping. |
| 94 | + |
| 95 | +## Choosing interfaces |
| 96 | + |
| 97 | +A mapping's interfaces resolve against each accepting node's own list, so one |
| 98 | +list covers nodes whose NICs are named differently. Given a class that gives |
| 99 | +worker-1 wt0 and enp1s0, and worker-2 wt0 and eth0: |
| 100 | + |
| 101 | +| The mapping asks for | worker-1 binds | worker-2 binds | |
| 102 | +| --- | --- | --- | |
| 103 | +| nothing | wt0, enp1s0 | wt0, eth0 | |
| 104 | +| wt0, enp1s0, eth0 | wt0, enp1s0 | wt0, eth0 | |
| 105 | +| wt0 | wt0 | wt0 | |
| 106 | +| enp1s0 | enp1s0 | nothing | |
| 107 | + |
| 108 | +Asking for wt0 alone keeps a mapping on the overlay. Asking for the LAN |
| 109 | +interfaces as well as wt0 reaches clients on both. |
| 110 | + |
| 111 | +An interface name that goes nowhere is reported, in one of three places |
| 112 | +depending on where the mistake is: |
| 113 | + |
| 114 | +| Mistake | Reported as | Where | |
| 115 | +| --- | --- | --- | |
| 116 | +| The mapping names an interface no node in the class carries | Accepted=False, InterfaceNotInClass | the PortMap | |
| 117 | +| The mapping names interfaces the class carries, none of them on the node that ended up serving | Programmed=False, NoInterfaceOnNode | the PortMap | |
| 118 | +| The class gives a node an interface the host does not have | the node row is not ready, `interface eth0 not present` | the PortMapClass, and as Programmed=False, NodeNotReady on each mapping it serves | |
| 119 | + |
| 120 | +A name some node carries is accepted, since binding on a subset of the nodes is |
| 121 | +the point of the field. The second row catches the case that gets through: a |
| 122 | +mapping admitted by the class check whose serving node has none of the names it |
| 123 | +asked for, which would otherwise program nothing in silence. |
| 124 | + |
| 125 | +## PortMap status |
| 126 | + |
| 127 | +```yaml |
| 128 | +status: |
| 129 | + endpoint: |
| 130 | + node: worker-b |
| 131 | + address: 10.244.18.107 |
| 132 | + published: |
| 133 | + - node: edge-a |
| 134 | + interface: enp1s0 |
| 135 | + address: 203.0.113.9 |
| 136 | + - node: edge-a |
| 137 | + interface: wt0 |
| 138 | + address: 100.64.93.143 |
| 139 | + observedGeneration: 3 |
| 140 | + conditions: |
| 141 | + - type: Accepted |
| 142 | + status: "True" |
| 143 | + reason: Valid |
| 144 | + - type: Programmed |
| 145 | + status: "True" |
| 146 | + reason: AllNodesReady |
| 147 | +``` |
| 148 | + |
| 149 | +`published` is what a person needs: the addresses this port answers on right |
| 150 | +now, one row per class interface on the mapping's serving node. It is what |
| 151 | +`kubectl get portmap` prints in its wide columns. |
| 152 | + |
| 153 | +The serving node writes the mapping's status, and every published row names one |
| 154 | +of its own interfaces, so it resolves each address off its own host. A row whose |
| 155 | +interface has not resolved yet carries an empty address and fills in on the next |
| 156 | +pass. |
| 157 | + |
| 158 | +| Condition | True when | Notable false reasons | |
| 159 | +| --- | --- | --- | |
| 160 | +| Accepted | the class exists, selects this namespace, the port range is inside the class range and unreserved, and no earlier mapping holds it | `ClassNotFound`, `NamespaceNotSelected`, `PortOutOfRange`, `PortReserved`, `PortConflict`, `InvalidPortRange` | |
| 161 | +| Programmed | the serving node has written its rules and its class status row reports ready, and for a pod on another node the return link has landed with usable addresses at both ends | `NoReadyEndpoint`, `ReturnPathUnavailable`, `NodeNotReady` | |
| 162 | + |
| 163 | +`InvalidPortRange` is the ceiling on the CEL rule that endPort must not be below |
| 164 | +port: an update the API server let through still reports here. |
| 165 | + |
| 166 | +`Programmed` gates on the participants: the serving node's row in the class |
| 167 | +status, and for a remote pod the landed link slot. Under Single the other |
| 168 | +accepting nodes hold nothing for the mapping, so their rows gate nothing. Until |
| 169 | +a gate clears, the status names what is missing, and the level-driven reconcile |
| 170 | +heals it. |
| 171 | + |
| 172 | +## Class status |
| 173 | + |
| 174 | +The class reports each selected node's readiness verdict and which return links |
| 175 | +hold an address slot. |
| 176 | + |
| 177 | +```yaml |
| 178 | +status: |
| 179 | + links: |
| 180 | + - key: edge-a/worker-b |
| 181 | + peers: [edge-a, worker-b] |
| 182 | + subnet: 169.254.77.2/31 |
| 183 | + slot: 1 |
| 184 | + nodes: |
| 185 | + - name: edge-a |
| 186 | + ready: true |
| 187 | + underlayMTU: 1400 |
| 188 | + linkMTU: 1350 |
| 189 | + addresses: |
| 190 | + enp1s0: 203.0.113.9 |
| 191 | + wt0: 100.64.93.143 |
| 192 | + - name: worker-c |
| 193 | + ready: false |
| 194 | + message: interface wt0 not present |
| 195 | + observedGeneration: 2 |
| 196 | + conditions: |
| 197 | + - type: Ready |
| 198 | + status: "True" |
| 199 | + reason: Valid |
| 200 | +``` |
| 201 | + |
| 202 | +One `links` entry per node pair that has, or recently had, a return link. The |
| 203 | +`unusedSince` field appears when no PortMap needs the pair and is set to the |
| 204 | +time that became true; an entry unused for 24h is dropped. The slot fixes the |
| 205 | +routing table (`200 + slot`), the packet mark (`0x6b700000 | slot`) and the |
| 206 | +link's VNI, so a claim is never renumbered while a link uses it. |
| 207 | + |
| 208 | +One `nodes` row per accepting node, written by that node's agent, carrying the |
| 209 | +MTU numbers and interface addresses it read from the host. The row is ready when |
| 210 | +every interface the class gives that node resolves to an address there; |
| 211 | +otherwise it carries a message naming the first interface that does not. The |
| 212 | +class names each node's interfaces, so an interface that fails to resolve is a |
| 213 | +mistake in the class rather than a node that lacks the NIC, and the row says so |
| 214 | +rather than skipping it. |
| 215 | + |
| 216 | +`linkMTU` is that node's own figure, `underlayMTU` less the encapsulation. A |
| 217 | +link device takes the smaller of its two ends' figures, so a link to a peer on a |
| 218 | +thinner underlay carries less than either row suggests. [MTU in |
| 219 | +troubleshooting.md](troubleshooting.md#mtu) has the reading. |
| 220 | + |
| 221 | +| Condition | True when | Notable false reasons | |
| 222 | +| --- | --- | --- | |
| 223 | +| Ready | the subnet parses, has a free slot, and the link port does not collide with the CNI's | `VxlanPortConflict`, `SubnetExhausted`, `InvalidSubnet` | |
| 224 | + |
| 225 | +`TunnelModeRequired` was a fourth reason until v0.4.0, when both legs moved onto |
| 226 | +kuport's own return link and the CNI's routing mode stopped deciding anything. |
0 commit comments