-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Timeout For TLS Passthrough #2504
Add Timeout For TLS Passthrough #2504
Conversation
Codecov Report
@@ Coverage Diff @@
## master #2504 +/- ##
==========================================
+ Coverage 40.58% 40.68% +0.09%
==========================================
Files 75 75
Lines 5098 5108 +10
==========================================
+ Hits 2069 2078 +9
- Misses 2750 2751 +1
Partials 279 279
Continue to review full report at Codecov.
|
/assign @aledbf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One extra suggestion: the first line of the commit message should be shorter and self-explanatory. The verbose description can stay on the following lines of course.
@@ -346,6 +346,11 @@ type Configuration struct { | |||
// https://www.nginx.com/resources/admin-guide/proxy-protocol/ | |||
UseProxyProtocol bool `json:"use-proxy-protocol,omitempty"` | |||
|
|||
// When use-proxy-protocol is enabled, sets the maximum time the connection handler will wait | |||
// to receieve proxy headers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"receive" (typo)
internal/ingress/controller/nginx.go
Outdated
@@ -722,7 +723,10 @@ func (n *NGINXController) setupSSLProxy() { | |||
glog.Fatalf("%v", err) | |||
} | |||
|
|||
proxyList := &proxyproto.Listener{Listener: listener} | |||
//Set a deadline to prevent the connection from hanging/blocking if a non proxy-proto connection is received. | |||
proxyDeadlineDuration, _ := time.ParseDuration(cfg.ProxyProtocolHeaderTimeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parsing the user input may fail, so the error should be handled.
internal/ingress/controller/nginx.go
Outdated
@@ -722,7 +723,10 @@ func (n *NGINXController) setupSSLProxy() { | |||
glog.Fatalf("%v", err) | |||
} | |||
|
|||
proxyList := &proxyproto.Listener{Listener: listener} | |||
//Set a deadline to prevent the connection from hanging/blocking if a non proxy-proto connection is received. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space after the comment mark.
Lets say warn and use the default value instead? |
That's also fine yes. Can you also add a test for this? |
I went ahead and moved configmap entry validation logic to the configmap template processors to stay consistent with where I see similar validations taking place. Also added a unit test to validate the input. I think it would be great to have an e2e test, but that really means we need a e2e for TLS passthrough in general. I can do that, but I think it goes beyond the scope of this PR and it could be added in a separate PR. |
Anything else needed on my part for this to move forward? |
Sorry for the delay, I will look into these changes today. Thanks for addressing my comments! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only minor comments on my side. I think using a time.Duration
type instead of a string would make a lot of sense for this particular scenario.
@@ -125,6 +127,17 @@ func ReadConfig(src map[string]string) config.Configuration { | |||
} | |||
} | |||
|
|||
//Verify that the configured timeout is parsable as a duration. if not, set the default value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space after //
internal/ingress/controller/nginx.go
Outdated
@@ -722,7 +723,15 @@ func (n *NGINXController) setupSSLProxy() { | |||
glog.Fatalf("%v", err) | |||
} | |||
|
|||
proxyList := &proxyproto.Listener{Listener: listener} | |||
// Set a deadline to prevent the connection from hanging/blocking if a non proxy-proto connection is received. | |||
proxyDeadlineDuration, err := time.ParseDuration(cfg.ProxyProtocolHeaderTimeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about making cfg.ProxyProtocolHeaderTimeout
an actual time.Duration
and benefit from the type safety? This way you would only have to care about errors when a user input is submitted.
@@ -31,6 +31,24 @@ func TestFilterErrors(t *testing.T) { | |||
} | |||
} | |||
|
|||
func TestProxytTimeoutParsing(t *testing.T) { | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leading newline.
if cfg.ProxyProtocolHeaderTimeout != "5s" { | ||
t.Errorf("expected 5s but got %v", cfg.ProxyProtocolHeaderTimeout) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing newline.
@@ -31,6 +31,24 @@ func TestFilterErrors(t *testing.T) { | |||
} | |||
} | |||
|
|||
func TestProxytTimeoutParsing(t *testing.T) { | |||
|
|||
cfg := ReadConfig(map[string]string{"proxy-protocol-header-timeout": "35s"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd use a map here in order to remain as consistent as possible with other tests. E.g.
testCases := map[string]struct {
input string
expect string // or time.Duration, see previous comment
}{
"valid duration": {"35s", "35s" }, // or 35*time.Second
"invalid duration": {"3zxs", "5s" }, // or 5*time.Second
}
for n, tc := range testCases {
// ...
}
Thanks for the feedback and guidance @antoineco I agree that using a Duration is a better approach and have adjusted the config type accordingly. I also think that change to unit testing is much cleaner. Both changes have been committed along with the styling fixes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @jrthrawny, it looks good to me now! Just one suggestion to address regarding the use of time.Duration
and I guess we're good to go.
@@ -528,6 +534,10 @@ func NewDefault() Configuration { | |||
defIPCIDR = append(defIPCIDR, "0.0.0.0/0") | |||
defNginxStatusIpv4Whitelist = append(defNginxStatusIpv4Whitelist, "127.0.0.1") | |||
defNginxStatusIpv6Whitelist = append(defNginxStatusIpv6Whitelist, "::1") | |||
defProxyDeadlineDuration, err := time.ParseDuration("5s") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could assign the variable to a time.Duration
directly and remove any possibility of failing at run time due to an incorrect input. eg. time.Duration(5000000000)
or (more readable) time.Duration(5) * time.Second
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. This should have been my initial approach
func TestProxytTimeoutParsing(t *testing.T) { | ||
testCases := map[string]struct { | ||
input string | ||
expect float64 // duration in seconds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same suggestion, use time.Duration
instead of assuming we use seconds and set the expect
field to something like time.Duration(5) * time.Second
in your test cases. This way a test case like { "100ms", time.Duration(100) * time.Millisecond }
is both valid and readable.
/approve |
@antoineco if you add lgtm we are ready to go |
@jrthrawny thank you for the PR and apologies for the delay in merging this |
Let's squash these 5 commits and use a descriptive commit title like "Configurable Proxy Protocol header timeout for TLS passthrough". The PR title is truncated. The documentation is also missing in docs/user-guide/nginx-configuration/configmap.md. |
1f10c6c
to
d637a9b
Compare
/lgtm |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: aledbf, antoineco, jrthrawny The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
What this PR does / why we need it:
Requests to the HTTPS port will hang indefintely under the following conditions:
Since enabling proxy protocol support is a global setting and cannot be changed for HTTP and HTTPs independently, this causes users of the NLB to need to disable proxy protocol for HTTPS to avoid errors on header processing if they have tls-passthrough enabled.
Which issue this PR fixes:
The go-proxyproto library specifically recommends settings a deadline if you call before call RemoteAddr() before calling Read() or it could cause the thread to sit in i/o wait if the proxy headers never come through. This can happen just due to network disruptions when a client is sending a request without the proxy headers and the rest of the request never makes it through.
This PR adds a configurable timeout/deadline value, defaulting 5 seconds to prevent this from occurring. I should also note that the /healthz does not currently check for the liveness of the proxy pass process, so when this bug does occur, it will not self heal just with the kube liveness probe.
Special notes for your reviewer:
The bug is reproduced by running the ingress controller in master with the following settings
cli:
--enable-tls-passthrough
configMap:
use-proxy-protocol: true
kubectl port-forward directly to a/the pod on port 443. Curls against the pod should return the default backend. To produce the bug. Telnet to the pod on 443 and do not put additional input. If you curl on a seperate terminal, might take a few attempts, you will see that the curl request will hang until clientside timeout. This is resolved by closing the telnet process or restarting the pod.