K8S Ingress環境下,Http Redirect端口丟失問題

githubhtml

近日發現一個問題:應用程序在返回Http Redirect的時候丟失了原先訪問的端口。好比,咱們這樣訪問http://IP-A:Port-A/app/delete,這個url會響應302,可是它返回的Response header Location裏丟失了端口,正確的結果應該是這樣:http://IP-A:Port-A/app/index,但返回的倒是:http://IP-A/app/index,把端口丟失了。java

基本狀況

咱們的部署狀況是這樣的:nginx

  • 部署了Nginx Ingress,並使用NodePort的方式把Nginx Ingress Service暴露出來
  • 配置了App的Ingress

服務器信息:git

Server Name NAT Server K8S Node Nginx Ingress Svc Nginx Ingress Pod App Svc App Pod
IP IP-A IP-B IP-C(Cluster IP/VIP) IP-D(Cluster IP) IP-E(Cluster IP/VIP) IP-F(ClusterIP)
Port Port-A Port-B(Nginx Ingress Svc's NodePort) Port-C 80(Container Port) Port-E Port-F

其實以上也不全是服務器,其中有兩個K8S Service不是服務器,它們是VIP,關於這個請看K8S - Using Source IP一文,當訪問http://IP-A:Port-A/app/delete的時候,這個請求從左到右貫穿了這些服務器。github

順便一提上面的NAT Server是一臺普通的服務器,咱們用它作了PAT使咱們的Nginx Ingress可以被外網訪問到。apache

觀察

咱們使用以前提到過的Echo Server來觀察透過Ingress訪問Echo Server時傳遞給Echo Server的Request header:http://IP-A:Port-A/echo-server,獲得了這些有趣的Request header:segmentfault

host=IP-A:Port-A
x-original-uri=/echo-server
x-forwarded-for=IP-B
x-forwarded-host=IP-A:Port-A
x-forwarded-port=80
x-forwarded-proto=http

而後直接訪問Echo Server Svc,發現是沒有上面提到的x-*Request header的。因而懷疑問題出在這幾個header上。api

名詞解釋

來說一下這些頭各自表明什麼意思。瀏覽器

  • x-forwarded-for,client訪問proxy的時候,client的ip。
    在這裏之因此是K8S Node的IP,是由於在Nginx Ingress看來請求是來自K8S Node的(好好看看以前提到的K8S - Using Source IP一文),在這以前的NAT它是不知道的。
  • x-forwarded-host,client訪問proxy的時候,訪問的原始host。
  • x-forwarded-proto,client訪問proxy的時候,訪問的原始http scheme。
  • x-forwarded-port,client訪問proxy的時候,訪問的port。
  • x-original-uri,查不到權威資料。

注意,前三個是事實標準,MDN有收錄,x-forwarded-portx-original-uri彷佛是私有擴展。tomcat

實驗

找一個趁手的Http Request工具(我用的是Postman),記得把Follow redirect關掉,而後模擬Nginx請求的方式(就是把上面提到的x-* header帶上/去掉/修改值)直接請求App Svc。

結果發現x-forwarded-port是Response header Location的關鍵,即若是x-forwarded-port=Port-A的話,Location就會帶上正確的端口。

分析

Redirect url是如何構造的

能夠推測,App利用了hostx-forwarded-*這些header來構造redirect url。

在Java Servlet API中,在描述HttpServletResponse#sendRedirect的時候提到,其返回的URL必須是Absolute URL。

Tomcat的org.apache.catalina.connector.ResponsetoAbsolute方法負責構造Absolute URL。

那麼它又是如何知道選用什麼Port的呢?這個和RemoteIPValve有關,有興趣的話你能夠查閱相關文檔。

上面只是講了Tomcat是如何構造redirect url的,但這個方法不是標準的,不一樣的容器有各自的實現,畢竟Java Servlet API也沒有規定如何構造Absolute URL。

我以前也寫過一篇相關話題的文章《反向代理使用https協議,後臺tomcat使用http,redirect時使用錯誤協議的解決辦法》,你能夠看一看。

爲什麼x-forwarded-port是80

那麼問題來了,我明明訪問的是IP-A:Port-A,爲什麼Nginx取到的值是80?

這是由於在整個請求鏈路的前段:NAT Server > K8S Node > Nginx Ingress Svc 都是在第4層工做的,能夠認爲它們乾的事情都是NAT,Nginx Ingress Pod是不知道這些服務器/網絡節點的端口,所以它只能把本身的端口80(容器內Port)給x-forwarded-port。

關於這個邏輯你能夠查看Nginx Ingress的配置文件就可以知道了:

kubectl -n kube-system exec -it <nginx-ingress-controller-pod-name> -- cat /etc/nginx/nginx.conf

解決辦法

請求時帶上x-forwarded-port(不靠譜)

查看Nginx Ingress配置文件發現若是最初請求的時候帶上x-forwarded-port的話,就可以改變它傳遞到後面的值,可是這有兩個問題:

  1. 經過瀏覽器訪問時,你沒有辦法加上這個header
  2. 這個header通常都是反向代理加的,也就是在咱們的Nginx Ingress以前還得有一個反向代理

因此這個方法很差。

修改tomcat的代碼(不靠譜)

雖然能夠經過修改tomcat的代碼,讓它從x-forward-host/host header來取port,可是這個不現實。

修改NAT Server的端口爲80(靠譜)

這個方法比較靠譜,只要將NAT Server的端口改爲80就沒有問題了。

事實上,若是你直接訪問K8S Node的話(NodePort方式),也是要將NodePort設置爲80,記得前面說的嗎?Nginx Ingress沒法知道上層NAT的端口。

總而言之,就是你最初請求的URL不能是80以外的端口,必須是http://some-ip/app才能夠。

使用Nginx Ingress Annotations(靠譜)

使用Nginx Ingress提供的Proxy redirect annotations,將Location的值作文本替換。

相關文章
相關標籤/搜索