Compare commits
1
Commits
next
..
7ad4bb7426
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ad4bb7426 |
No files matched your search
@@ -298,7 +298,7 @@ Every setting the code reads, with its default:
|
|||||||
| `SWW_RPROXY_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
| `SWW_RPROXY_LISTEN_ADDR` | `:8080` | Where `rproxy` listens; leave the host part empty. |
|
||||||
| `SWW_RPROXY_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
| `SWW_RPROXY_TRUSTED_PROXIES` | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | The netblocks whose `X-Forwarded-For` `rproxy` believes; set but empty, it believes none. |
|
||||||
| `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `waf`'s answer to a question and to a report, in milliseconds. |
|
| `SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS` | `2000` | How long `rproxy` waits for `waf`'s answer to a question and to a report, in milliseconds. |
|
||||||
| `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` | `allow` | What `rproxy` does with a request when `waf` gives no answer in time, and while the circuit breaker for asks to `waf` is open: `allow` passes it to the app, `deny` answers `503`. |
|
| `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` | `allow` | What `rproxy` does with a request when `waf` cannot be reached or does not answer in time: `allow` passes it to the app, `deny` answers `503`. |
|
||||||
| `SWW_RPROXY_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
| `SWW_RPROXY_MAX_REQUEST_SIZE` | `100M` | The largest request body, a size or `off`; a larger one is answered `413`. |
|
||||||
| `SWW_RPROXY_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`. The visitor gets `502` in place of a larger one, or it is cut off if it has already started. |
|
| `SWW_RPROXY_MAX_RESPONSE_SIZE` | `1G` | The largest response body, a size or `off`. The visitor gets `502` in place of a larger one, or it is cut off if it has already started. |
|
||||||
| `SWW_RPROXY_MAX_REQUEST_TIME_MS` | `60000` | Two time limits, each this long, in milliseconds or `off`: one for the visitor to send its request line and headers, then one for the whole request to reach the app. |
|
| `SWW_RPROXY_MAX_REQUEST_TIME_MS` | `60000` | Two time limits, each this long, in milliseconds or `off`: one for the visitor to send its request line and headers, then one for the whole request to reach the app. |
|
||||||
|
|||||||
@@ -2584,10 +2584,11 @@ request and ban notes name the site by its host.
|
|||||||
|
|
||||||
`sww rproxy` is the reverse proxy in front of one app. It asks `waf` about each
|
`sww rproxy` is the reverse proxy in front of one app. It asks `waf` about each
|
||||||
request, and passes on what `waf` allows. It writes nothing to disk. The only
|
request, and passes on what `waf` allows. It writes nothing to disk. The only
|
||||||
thing it keeps between requests is its circuit breaker for asks to `waf`.
|
thing it keeps between requests is its circuit breaker for asks to `waf`. When
|
||||||
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` decides what is done with a
|
`waf` cannot be connected to, does not answer at all, or does not answer within
|
||||||
request when `waf` gives no answer in time, and while the circuit breaker is
|
`SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS`,
|
||||||
open. Its default, `allow`, keeps the site answering.
|
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` decides. Its default, `allow`,
|
||||||
|
keeps the site answering.
|
||||||
|
|
||||||
`rproxy` reads only its `SWW_RPROXY_` settings, which `docs/settings.md` lists.
|
`rproxy` reads only its `SWW_RPROXY_` settings, which `docs/settings.md` lists.
|
||||||
One of them is required: `SWW_RPROXY_WAF_BACKEND`, `waf`'s URL. It has no
|
One of them is required: `SWW_RPROXY_WAF_BACKEND`, `waf`'s URL. It has no
|
||||||
@@ -2605,8 +2606,7 @@ only. The image runs it by default (see "Deployment").
|
|||||||
4. It refuses a request with a method it does not pass on, or one it cannot put
|
4. It refuses a request with a method it does not pass on, or one it cannot put
|
||||||
to `waf` as the visitor sent it (see "Requests `rproxy` refuses itself").
|
to `waf` as the visitor sent it (see "Requests `rproxy` refuses itself").
|
||||||
5. It reads up to the first 64 KiB of the body into memory.
|
5. It reads up to the first 64 KiB of the body into memory.
|
||||||
6. It sends `waf` the question (see "The question"), unless the circuit breaker
|
6. It sends `waf` the question (see "The question"). It speaks HTTP/1.1 to
|
||||||
keeps it from asking (see "The circuit breaker"). It speaks HTTP/1.1 to
|
|
||||||
`waf`, whatever `waf` offers. Go's HTTP/2 client would refuse to send a
|
`waf`, whatever `waf` offers. Go's HTTP/2 client would refuse to send a
|
||||||
visitor's `Upgrade` header, and most `Connection` headers. The question never
|
visitor's `Upgrade` header, and most `Connection` headers. The question never
|
||||||
carries the visitor's `Trailer`.
|
carries the visitor's `Trailer`.
|
||||||
@@ -2715,10 +2715,9 @@ status `0` and no `disposition`.
|
|||||||
### The circuit breaker
|
### The circuit breaker
|
||||||
|
|
||||||
`rproxy` stops asking `waf` for a while when `waf` keeps failing to answer. An
|
`rproxy` stops asking `waf` for a while when `waf` keeps failing to answer. An
|
||||||
ask fails when `waf` gives no answer in time (see "Without an answer from
|
ask fails in each case that "Without an answer from `waf`" lists. Any answer in
|
||||||
`waf`"), or an answer `rproxy` cannot pass on. Any other answer in time
|
time succeeds, whatever it decides, and so does an answer `rproxy` cannot pass
|
||||||
succeeds, whatever it decides. A visitor that goes away while `waf` is being
|
on. A visitor that goes away while `waf` is being asked counts as neither.
|
||||||
asked counts as neither.
|
|
||||||
|
|
||||||
The breaker works like this:
|
The breaker works like this:
|
||||||
|
|
||||||
@@ -2728,8 +2727,8 @@ The breaker works like this:
|
|||||||
It gets the same process warning and `waf_backend_no_answer` as a request
|
It gets the same process warning and `waf_backend_no_answer` as a request
|
||||||
whose ask failed.
|
whose ask failed.
|
||||||
- While it is open, one request a second still asks `waf`.
|
- While it is open, one request a second still asks `waf`.
|
||||||
- Each ask that succeeds clears the failures counted so far, and closes the
|
- The first ask that succeeds closes it, and the failures before it no longer
|
||||||
breaker if it is open.
|
count.
|
||||||
|
|
||||||
The breaker lives in memory, and a restart of `rproxy` closes it. It has no
|
The breaker lives in memory, and a restart of `rproxy` closes it. It has no
|
||||||
settings.
|
settings.
|
||||||
|
|||||||
+1
-2
@@ -238,8 +238,7 @@ meanings where they differ:
|
|||||||
- `disposition`: what was done with the request: `allow`, `deny` or `close`. It
|
- `disposition`: what was done with the request: `allow`, `deny` or `close`. It
|
||||||
is `waf`'s answer, or the value of
|
is `waf`'s answer, or the value of
|
||||||
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION`. It is left out when:
|
`SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION`. It is left out when:
|
||||||
- `waf` was not asked and `SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION` did
|
- `rproxy` answered the request itself, as for the health check;
|
||||||
not apply, as for the health check;
|
|
||||||
- the visitor went away while `waf` was being asked;
|
- the visitor went away while `waf` was being asked;
|
||||||
- `waf`'s answer had no valid `X-Sww-Disposition`;
|
- `waf`'s answer had no valid `X-Sww-Disposition`;
|
||||||
- `rproxy` could not pass on `waf`'s answer, and answered `502`.
|
- `rproxy` could not pass on `waf`'s answer, and answered `502`.
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ var errBreakerOpen = errors.New(
|
|||||||
|
|
||||||
// breaker is the circuit breaker for asks to waf. It opens when
|
// breaker is the circuit breaker for asks to waf. It opens when
|
||||||
// breakerFailures asks fail within breakerWindow. An ask fails when it gets no
|
// breakerFailures asks fail within breakerWindow. An ask fails when it gets no
|
||||||
// answer in time, or an answer rproxy cannot pass on. While it is open, at
|
// answer in time. While it is open, at most one request each breakerAskInterval
|
||||||
// most one request each breakerAskInterval asks waf. Any usable answer closes
|
// asks waf. The first answer closes it, whatever it decides.
|
||||||
// it, whatever it decides, and forgets the failures before it.
|
|
||||||
type breaker struct {
|
type breaker struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
// failures holds the times of the latest failed asks, oldest first, at most
|
// failures holds the times of the latest failed asks, oldest first, at most
|
||||||
@@ -53,14 +52,16 @@ func (b *breaker) mayAsk(now time.Time) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// noteAnswer notes that an ask got an answer rproxy can use. It closes the
|
// noteAnswer notes that an ask got an answer in time. It closes the breaker,
|
||||||
// breaker and forgets the failures before it.
|
// and forgets the failures before it.
|
||||||
func (b *breaker) noteAnswer() {
|
func (b *breaker) noteAnswer() {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
|
if b.open {
|
||||||
b.open = false
|
b.open = false
|
||||||
b.failures = nil
|
b.failures = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// noteFailure notes that an ask that ended at now got no answer in time, and
|
// noteFailure notes that an ask that ended at now got no answer in time, and
|
||||||
|
|||||||
@@ -63,17 +63,17 @@ func TestFiveFailedAsksOverMoreThanFiveSecondsLeaveTheBreakerClosed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnAnswerBetweenFailedAsksKeepsTheBreakerClosed(t *testing.T) {
|
func TestAnAnswerBeforeTheBreakerOpensDoesNotForgetFailures(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
b := &rproxy.Breaker{}
|
b := &rproxy.Breaker{}
|
||||||
fail(b, now, 4, 100*time.Millisecond)
|
fail(b, now, 4, time.Millisecond)
|
||||||
b.NoteAnswer()
|
b.NoteAnswer()
|
||||||
fail(b, now.Add(500*time.Millisecond), 4, 100*time.Millisecond)
|
b.NoteFailure(now.Add(time.Second))
|
||||||
|
|
||||||
if !b.MayAsk(now.Add(900 * time.Millisecond)) {
|
if b.MayAsk(now.Add(time.Second)) {
|
||||||
t.Error("a request may not ask, want it to")
|
t.Error("a request may ask, want it not to")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func (b *Breaker) MayAsk(now time.Time) bool {
|
|||||||
return b.b.mayAsk(now)
|
return b.b.mayAsk(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoteAnswer notes that an ask got an answer rproxy can use.
|
// NoteAnswer notes that an ask got an answer in time.
|
||||||
func (b *Breaker) NoteAnswer() {
|
func (b *Breaker) NoteAnswer() {
|
||||||
b.b.noteAnswer()
|
b.b.noteAnswer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1490,7 +1490,7 @@ func TestVisitorsThatGoAwayWhileWAFIsAskedLeaveTheBreakerClosed(
|
|||||||
standIn.question(t)
|
standIn.question(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnswersThatCannotBePassedOnOpenTheBreaker(t *testing.T) {
|
func TestAnswersThatCannotBePassedOnLeaveTheBreakerClosed(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var answering atomic.Bool
|
var answering atomic.Bool
|
||||||
@@ -1506,10 +1506,11 @@ func TestAnswersThatCannotBePassedOnOpenTheBreaker(t *testing.T) {
|
|||||||
tooLarge(w, r)
|
tooLarge(w, r)
|
||||||
})
|
})
|
||||||
app := startApp(t, greet)
|
app := startApp(t, greet)
|
||||||
addr, out := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
addr, _ := startRProxy(t, standIn.URL, app.URL, map[string]string{
|
||||||
wafBackendTimeout: "60000",
|
wafBackendTimeout: "60000",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Five of these would open the breaker if they counted as failures.
|
||||||
for range 5 {
|
for range 5 {
|
||||||
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
|
if got := get(t, addr, "/"); got.status != http.StatusBadGateway {
|
||||||
t.Errorf("answered %d, want 502", got.status)
|
t.Errorf("answered %d, want 502", got.status)
|
||||||
@@ -1520,13 +1521,7 @@ func TestAnswersThatCannotBePassedOnOpenTheBreaker(t *testing.T) {
|
|||||||
|
|
||||||
answering.Store(true)
|
answering.Store(true)
|
||||||
wantGreeting(t, get(t, addr, "/"))
|
wantGreeting(t, get(t, addr, "/"))
|
||||||
out.waitFor(t, "the circuit breaker is open")
|
standIn.question(t)
|
||||||
|
|
||||||
select {
|
|
||||||
case <-standIn.questions:
|
|
||||||
t.Error("waf was asked, want it not to be")
|
|
||||||
case <-time.After(10 * pollInterval):
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPathUnderOwnPrefixNeverReachesTheApp(t *testing.T) {
|
func TestAPathUnderOwnPrefixNeverReachesTheApp(t *testing.T) {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func (rq *request) handleNoUsableAnswer(ctx context.Context, err error) {
|
|||||||
|
|
||||||
// askUnlessBreakerOpen asks waf, unless the circuit breaker is open and it is
|
// askUnlessBreakerOpen asks waf, unless the circuit breaker is open and it is
|
||||||
// not yet time for a request to ask. It then tells the breaker whether waf
|
// not yet time for a request to ask. It then tells the breaker whether waf
|
||||||
// answered in time. An answer rproxy cannot pass on counts as a failure. A
|
// answered in time. An answer rproxy cannot pass on still came in time. A
|
||||||
// visitor that goes away tells the breaker nothing. ctx is the request's own
|
// visitor that goes away tells the breaker nothing. ctx is the request's own
|
||||||
// context.
|
// context.
|
||||||
func (rq *request) askUnlessBreakerOpen(
|
func (rq *request) askUnlessBreakerOpen(
|
||||||
@@ -104,7 +104,7 @@ func (rq *request) askUnlessBreakerOpen(
|
|||||||
answer, err := rq.ask(ctx)
|
answer, err := rq.ask(ctx)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case err == nil:
|
case err == nil, errors.Is(err, errCannotPassOn):
|
||||||
b.noteAnswer()
|
b.noteAnswer()
|
||||||
case ctx.Err() == nil:
|
case ctx.Err() == nil:
|
||||||
b.noteFailure(time.Now())
|
b.noteFailure(time.Now())
|
||||||
|
|||||||
Reference in new issue
Block a user