handleRelay is the main relay endpoint. Expected request: POST /relay/{clusterId} Content-Type: application/octet-stream Body: raw inner NHP packet bytes (KNK / RKN / etc., encrypted by agent) Response: 200 OK — body contains raw NHP ACK / COK packet bytes (encrypted to agent) 400 Bad Requ
(w http.ResponseWriter, r *http.Request)
| 851 | // 504 Gateway Timeout — NHP Server did not respond in time |
| 852 | // 502 Bad Gateway — internal error |
| 853 | func (rs *RelayServer) handleRelay(w http.ResponseWriter, r *http.Request) { |
| 854 | if r.Method != http.MethodPost { |
| 855 | http.Error(w, "method not allowed", http.StatusMethodNotAllowed) |
| 856 | return |
| 857 | } |
| 858 | |
| 859 | cr, status, errMsg := rs.resolveCluster(r) |
| 860 | if cr == nil { |
| 861 | http.Error(w, errMsg, status) |
| 862 | return |
| 863 | } |
| 864 | |
| 865 | // Read inner NHP packet from request body. Cap at maxPacketSize+1 so we |
| 866 | // can reject oversize bodies without pulling an unbounded amount into |
| 867 | // memory. A single r.Body.Read() is not guaranteed to return the full |
| 868 | // payload; io.ReadAll drains until EOF. |
| 869 | innerPacket, err := io.ReadAll(io.LimitReader(r.Body, int64(maxPacketSize)+1)) |
| 870 | if err != nil { |
| 871 | http.Error(w, "failed to read body", http.StatusBadRequest) |
| 872 | return |
| 873 | } |
| 874 | if len(innerPacket) == 0 { |
| 875 | http.Error(w, "empty packet", http.StatusBadRequest) |
| 876 | return |
| 877 | } |
| 878 | if len(innerPacket) > maxPacketSize { |
| 879 | http.Error(w, "packet too large", http.StatusBadRequest) |
| 880 | return |
| 881 | } |
| 882 | n := len(innerPacket) |
| 883 | |
| 884 | // Extract the counter from the inner packet header (bytes [16:24], big-endian uint64). |
| 885 | // The NHP server echoes this counter in its ACK/COK response, so we use it |
| 886 | // to match the response back to this HTTP request. |
| 887 | if n < 24 { |
| 888 | http.Error(w, "inner packet too short", http.StatusBadRequest) |
| 889 | return |
| 890 | } |
| 891 | innerCounter := binary.BigEndian.Uint64(innerPacket[16:24]) |
| 892 | |
| 893 | // Body is valid; pick a target instance. normalize() blocks an empty |
| 894 | // instance list in phase 1, but phase 2's health checks can legitimately |
| 895 | // empty the healthy pool, so surface that as a 503 instead of panicking |
| 896 | // on a nil dereference downstream. |
| 897 | inst := cr.pickInstance() |
| 898 | if inst == nil { |
| 899 | http.Error(w, "cluster has no usable instance", http.StatusServiceUnavailable) |
| 900 | return |
| 901 | } |
| 902 | |
| 903 | realAddr, err := realClientAddr(r) |
| 904 | if err != nil { |
| 905 | log.Error("[Relay] %v", err) |
| 906 | http.Error(w, "relay misconfigured: missing X-Real-IP header from local reverse proxy", http.StatusBadGateway) |
| 907 | return |
| 908 | } |
| 909 | realAddrKey := realAddr.String() |
| 910 | log.Info("[Relay] forwarding %d-byte inner packet (counter=%d, cluster=%s) from client %s to %s", |