Skip to content
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

proxyprotocol: use github.com/pires/go-proxyproto #5915

Merged
merged 6 commits into from Dec 13, 2023

Conversation

mohammed90
Copy link
Member

Further enhancements can be added later, e.g. advanced allow/denylist policies.

I'll test it out soon, unless someone beats me to the test.

@mohammed90 mohammed90 added under review 🧐 Review is pending before merging dependencies ⛓️ Pull requests that update a dependency file labels Oct 24, 2023
@francislavoie
Copy link
Member

/cc @Embraser01 @KorvinSzanto if you'd like to test this

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
@francislavoie francislavoie added this to the v2.9.0 milestone Oct 24, 2023
@francislavoie
Copy link
Member

The code looks good to me. Glad we could retain the same options as-is!

Copy link
Member

@WeidiDeng WeidiDeng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few nitpicks only, LGTM.

@@ -39,31 +38,29 @@ type ListenerWrapper struct {
// allow/require PROXY headers from.
Allow []string `json:"allow,omitempty"`

rules []proxyprotocol.Rule
policies []goproxy.PolicyFunc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why store this in a slice when there can be one func at once? proxyproto takes a slice of IP address and return a function. There is no need to store it in a slice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's my attempt at future-proofing. proxyproto has a selection of policies. We can also build more as long as they conform to the PolicyFunc type. My thinking is we could expand the implementation later to unionize policies.

ReadHeaderTimeout: time.Duration(pp.Timeout),
}
if len(pp.policies) > 0 {
pl.Policy = pp.policies[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use a field of type PolicyFunc and assign it here.

@KorvinSzanto
Copy link

This does seem to resolve #5863 for me.

@Embraser01
Copy link
Member

Embraser01 commented Oct 25, 2023

🎉 Can confirm it works as well as the one we made over at https://github.com/caddyserver/ingress/tree/master/pkg/proxy.

I created a PR to replace it by this one once merged and released

Copy link
Member

@mholt mholt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks for testing it out.

This is an elegant change. Once Weidi's comments are considered (or replied to) I'm good with merging this. Thank you @mohammed90 !!

@mohammed90
Copy link
Member Author

🎉 Can confirm it works as well as the one we made over at https://github.com/caddyserver/ingress/tree/master/pkg/proxy.

I created a PR to replace it by this one once merged and released

@Embraser01, I see your implementation uses the REQUIRE policy, while this PR goes with USE. Should I expose this configuration option?

@Embraser01
Copy link
Member

🎉 Can confirm it works as well as the one we made over at caddyserver/ingress@master/pkg/proxy.

I created a PR to replace it by this one once merged and released

@Embraser01, I see your implementation uses the REQUIRE policy, while this PR goes with USE. Should I expose this configuration option?

It could be nice to expose this option as some user may want to force proxy protocol. Currently the ingress controller doesn't let choose between those modes but I think it should probably be USE by default (don't remember why I set it up to REQUIRE)

Copy link
Member

@WeidiDeng WeidiDeng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing major except for the possible unix socket bug. I prefer to use the new netip types if possible.

modules/caddyhttp/proxyprotocol/policy.go Show resolved Hide resolved
modules/caddyhttp/proxyprotocol/listenerwrapper.go Outdated Show resolved Hide resolved
modules/caddyhttp/proxyprotocol/listenerwrapper.go Outdated Show resolved Hide resolved
@WeidiDeng
Copy link
Member

WeidiDeng commented Nov 4, 2023

LGTM, :shipit: .

@mholt mholt merged commit dc12bd9 into master Dec 13, 2023
24 checks passed
@mholt mholt deleted the switch-proxyprotocol-lib branch December 13, 2023 16:07
@mholt mholt removed the under review 🧐 Review is pending before merging label Dec 13, 2023
tianze0926 added a commit to tianze0926/caddy that referenced this pull request Dec 20, 2023
* templates: Fix httpInclude (fix caddyserver#5698)

Allowable during feature freeze because this is a simple, non-invasive
bug fix only.

* ci: Use gofumpt to format code (caddyserver#5707)

* go.mod: Upgrade golang.org/x/net to 0.14.0 (caddyserver#5718)

* ci: Add riscv64 (64-bit RISC-V) to goreleaser (caddyserver#5720)

This will add 64-bit RISC-V Linux prebuilts for Caddy.

* ci: Update to Go 1.21 (caddyserver#5719)

* ci: Update to Go 1.21

* Bump quic-go to v0.37.4

* Check EnableFullDuplex err

* Linter bug suppression

See timakin/bodyclose#52

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* fileserver: Don't repeat error for invalid method inside error context (caddyserver#5705)

* caddytls: Update docs for on-demand config

* Fix tests

I thought Go ordered JSON objects when marshaling, but I guess not.

* cmd: Require config for caddy validate (fix caddyserver#5612) (caddyserver#5614)

* Require config for caddy validate - fixes caddyserver#5612

Signed-off-by: Pistasj <hi@pistasjis.net>

* Try making adjacent Caddyfile check its own function

Signed-off-by: Pistasj <hi@pistasjis.net>

* add Francis' suggestion

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Refactor

* Fix borked commit, sigh

---------

Signed-off-by: Pistasj <hi@pistasjis.net>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>

* fileserver: Slightly more fitting icons

* ci: use gci linter (caddyserver#5708)

* use gofmput to format code

* use gci to format imports

* reconfigure gci

* linter autofixes

* rearrange imports a little

* export GOOS=windows golangci-lint run ./... --fix

* reverseproxy: Always return new upstreams (fix caddyserver#5736) (caddyserver#5752)

* reverseproxy: Always return new upstreams (fix caddyserver#5736)

* Fix healthcheck logger race

* go.mod: Upgrade CertMagic and quic-go

* fix package typo (caddyserver#5764)

Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>

* fileserver: docs: clarify the ability to produce JSON array with `browse` (caddyserver#5751)

* caddyfile: Loosen heredoc parsing (caddyserver#5761)

* httpcaddyfile: Stricter errors for site and upstream address schemes (caddyserver#5757)

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* update quic-go to v0.37.6 (caddyserver#5767)

* caddyfile: Adjust error formatting (caddyserver#5765)

* replacer: change timezone to UTC for "time.now.http" placeholders (caddyserver#5774)

* chore: Appease gosec linter (caddyserver#5777)

These happen to be harmless memory aliasing
but I guess the linter can't know that and we
can't really prove it in general.

* go.mod: Update quic-go to v0.38.0 (caddyserver#5772)

* go.mod: Update quic-go to v0.38.0

* run "go mod tidy"

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* caddyfile: Fix case where heredoc marker is empty after newline (caddyserver#5769)

Fixes `panic: runtime error: slice bounds out of range [:3] with capacity 2`

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* ci: ensure short-sha is exported correctly on all platforms (caddyserver#5781)

* fileserver: Export BrowseTemplate

This allows programs embedding Caddy to customize the browse template.

* logging: Clone array on log filters, prevent side-effects (caddyserver#5786)

Fixes https://caddy.community/t/is-caddy-mutating-header-content-from-logging-settings/20947

* logging: query filter for array of strings (caddyserver#5779)

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* ci: Run govulncheck (caddyserver#5790)

* feat(ci): check vuln Go mods in CI

* fix(ci): correct directive for govulncheck

* refactor(ci): move govulncheck to lint.yml

* refactor(lint): move govulncheck to different job

* cmd: Prevent overwriting existing env vars with `--envfile` (caddyserver#5803)

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* httpcaddyfile: fix placeholder shorthands in named routes (caddyserver#5791)

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* reverseproxy: fix nil pointer dereference in AUpstreams.GetUpstreams (caddyserver#5811)

fix a nil pointer dereference in AUpstreams.GetUpstreams when AUpstreams.Versions is not set (fixes caddyserver#5809)

Signed-off-by: Pascal Vorwerk <info@fossores.de>

* fileserver: browse template SVG icons and UI tweaks (caddyserver#5812)

* fileserver browse.html UI tweaks: folder-symlink icon, search

fileserver browse.html UI tweaks: folder-symlink icon, search

- ui - add folder-symlink SVG icon
- search: use `<input type="search">` instead of `text`
- fix npe with `sizebar.style.width` = null in grid mode

* tabify whitespace

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* caddyhttp: Use LimitedReader for HTTPRedirectListener

* build(deps): bump actions/checkout from 3 to 4 (caddyserver#5846)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump goreleaser/goreleaser-action from 4 to 5 (caddyserver#5847)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: caddytest.AssertResponseCode error message (caddyserver#5853)

* reverseproxy: Allow fallthrough for response handlers without routes (caddyserver#5780)

* templates: Add dummy `RemoteAddr` to `httpInclude` request, proxy compatibility (caddyserver#5845)

* Enhancement: Allow X-Forwarded-For Header in httpInclude Virtual Requests

The goal of this enhancement is to modify the funcHTTPInclude function in the Caddy codebase to include the X-Forwarded-For header in the virtual request. This change will enable reverse proxies to set the X-Forwarded-For header, ensuring that the client's IP address is correctly provided to the target endpoint. This modification is essential for applications that depend on the X-Forwarded-For header for various functionalities, such as authentication, logging, or content customization.

* Updated tplcontext.go - set `virtReq.RemoteAddr = "127.0.0.1"`

i have made the suggested changes

* Apply suggestions from code review

* Update modules/caddyhttp/templates/tplcontext.go

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* go.mod: Upgrade dependencies incl. x/net/http

Possibly important for the HTTP/2 Rapid Reset issue.

* fileserver: Add command shortcuts `-l` and `-a` (caddyserver#5854)

* encode: Add `application/wasm*` to the default content types (caddyserver#5869)

* httpcaddyfile: Enable TLS for catch-all site if `tls` directive is specified (caddyserver#5808)

* reverseproxy: Fix retries on "upstreams unavailable" error (caddyserver#5841)

* reverseproxy: fix parsing Caddyfile fails for unlimited request/response buffers (caddyserver#5828)

* cmd: Fix exiting with custom status code, add `caddy -v` (caddyserver#5874)

* Simplify variables for commands

* Add --envfile support for adapt command

* Carry custom status code for commands to os.Exit()

* cmd: add `-v` and `--version` to root caddy command

* Add `--envfile` to `caddy environ`, extract flag parsing to func

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* httpcaddyfile: Sort TLS SNI matcher for deterministic JSON output (caddyserver#5860)

* httpcaddyfile: Sort TLS SNI matcher, for deterministic adapt output

* Update caddyconfig/httpcaddyfile/httptype.go

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* reverseproxy: Replace health header placeholders (caddyserver#5861)

* reverseproxy: Add logging for dynamic A upstreams (caddyserver#5857)

* reverseproxy: Fix `least_conn` policy regression (caddyserver#5862)

* reverseproxy: Add more debug logs (caddyserver#5793)

* reverseproxy: Add more debug logs

This makes debug logging very noisy when reverse proxying, but I guess
that's the point.

This has shown to be useful in troubleshooting infrastructure issues.

* Update modules/caddyhttp/reverseproxy/streaming.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Update modules/caddyhttp/reverseproxy/streaming.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Add opt-in `trace_logs` option

* Rename to VerboseLogs

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* tls: Add X25519Kyber768Draft00 PQ "curve" behind build tag (caddyserver#5852)

… when compiled with cfgo (https://github.com/cloudflare/go).

* fileserver: Set canonical URL on browse template (caddyserver#5867)

* Browse.html: Add canonical URL and home-link

When contents are equal, but maybe just a sort order is different, it is good to add `<link rel="canonical" href="base-path/" />`. This helps search engines propeely index the page.

I also added a link to the home page with the name of `{{.Host}}` just above the bread crumbs to make the page clearer.

https://paste.tnonline.net/files/28Wun5CQZiqA_Screenshot_20231007_134435_Opera.png

* Update browse.html

* ci: Force the Go version for govulncheck (caddyserver#5879)

* admin: Respond with 4xx on non-existing config path (caddyserver#5870)

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* caddyfile: Fix variadic placeholder false positive when token contains `:` (caddyserver#5883)

* cmd: upgrade: resolve symlink of the executable (caddyserver#5891)

* httpcaddyfile: Fix TLS automation policy merging with get_certificate (caddyserver#5896)

* templates: Clarify `include` args docs, add `.ClientIP` (caddyserver#5898)

* core: quic listener will manage the underlying socket by itself (caddyserver#5749)

* core: quic listener will manage the underlying socket by itself.

* format code

* rename sharedQUICTLSConfig to sharedQUICState, and it will now manage the number of active requests

* add comment

* strict unwrap type

* fix unwrap

* remove comment

* cmd: Add newline character to version string in CLI output (caddyserver#5895)

* caddyhttp: Use sync.Pool to reduce lengthReader allocations (caddyserver#5848)

* Use sync.Pool to reduce lengthReader allocations

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>

* Add defer putLengthReader to prevent leak

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>

* Cleanup in putLengthReader

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

---------

Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* core: Apply SO_REUSEPORT to UDP sockets (caddyserver#5725)

* core: Apply SO_REUSEPORT to UDP sockets

For some reason, 10 months ago when I implemented SO_REUSEPORT
for TCP, I didn't realize, or forgot, that it can be used for UDP too. It is a
much better solution than using deadline hacks to reuse a socket, at
least for TCP.

Then mholt/caddy-l4#132 was posted,
in which we see that UDP servers never actually stopped when the
L4 app was stopped. I verified this using this command:

    $ nc -u 127.0.0.1 55353

combined with POSTing configs to the /load admin endpoint (which
alternated between an echo server and a proxy server so I could tell
which config was being used).

I refactored the code to use SO_REUSEPORT for UDP, but of course
we still need graceful reloads on all platforms, not just Unix, so I
also implemented a deadline hack similar to what we used for
TCP before. That implementation for TCP was not perfect, possibly
having a logical (not data) race condition; but for UDP so far it
seems to be working. Verified the same way I verified that SO_REUSEPORT
works.

I think this code is slightly cleaner and I'm fairly confident this code
is effective.

* Check error

* Fix return

* Fix var name

* implement Unwrap interface and clean up

* move unix packet conn to platform specific file

* implement Unwrap for unix packet conn

* Move sharedPacketConn into proper file

* Fix Windows

* move sharedPacketConn and fakeClosePacketConn to proper file

---------

Co-authored-by: Weidi Deng <weidi_deng@icloud.com>

* httpcaddyfile: Remove port from logger names (caddyserver#5881)

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* templates: Delete headers on `httpError` to reset to clean slate (caddyserver#5905)

* go.mod: CVE-2023-45142 Update opentelemetry (caddyserver#5908)

* go.mod: Upgrade quic-go to v0.39.1

* caddyhttp: Adjust `scheme` placeholder docs (caddyserver#5910)

* Upgrade acmeserver to github.com/go-chi/chi/v5 (caddyserver#5913)

This commit upgrades the router used in the acmeserver to
github.com/go-chi/chi/v5. In the latest release of step-ca, the router
used by certificates was upgraded to that version.

Fixes caddyserver#5911

Signed-off-by: Mariano Cano <mariano.cano@gmail.com>

* test: acmeserver: add smoke test for the ACME server directory (caddyserver#5914)

* chore: Fix usage pool comment (caddyserver#5916)

* update quic-go to v0.39.3 (caddyserver#5918)

* go.mod: update quic-go version to v0.40.0 (caddyserver#5922)

* Revert "caddyhttp: Use sync.Pool to reduce lengthReader allocations (caddyserver#5848)" (caddyserver#5924)

* fileserver: Add .m4v for browse template icon

* httpredirectlistener: Only set read limit for when request is HTTP (caddyserver#5917)

* chore: Bump otel to v1.21.0. (caddyserver#5949)

Signed-off-by: Dan Lorenc <dlorenc@chainguard.dev>

* panic when reading from backend failed to propagate stream error (caddyserver#5952)

* http2 uses new round-robin scheduler (caddyserver#5946)

* templates: Offically make templates extensible (caddyserver#5939)

* templates: Offically make templates extensible

This supercedes caddyserver#4757 (and caddyserver#4568) by making template extensions
configurable.

The previous implementation was never documented AFAIK and had only
1 consumer, which I'll notify as a courtesy.

* templates: Add 'maybe' function for optional components

* Try to fix lint error

* tls: accept placeholders in string values of certificate loaders (caddyserver#5963)

* tls: loader: accept placeholders in string values

* appease the linter

* caddytls: Context to DecisionFunc (caddyserver#5923)

See caddyserver/certmagic#255

* caddytls: Sync distributed storage cleaning (caddyserver#5940)

* caddytls: Log out remote addr to detect abuse

* caddytls: Sync distributed storage cleaning

* Handle errors

* Update certmagic to fix tiny bug

* Split off port when logging remote IP

* Upgrade CertMagic

* chore: cross-build for AIX (caddyserver#5971)

* core: Always make AppDataDir for InstanceID (caddyserver#5976)

* cmd: Preserve LastModified date when exporting storage (caddyserver#5968)

* proxyprotocol: use github.com/pires/go-proxyproto (caddyserver#5915)

* proxyprotocol: use github.com/pires/go-proxyproto

* Fix typo: r/generelly/generally

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* add config options for `Deny` CIDR and fallback policy

* use `netip` package & trust unix sockets

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* caddyhttp: Add `uuid` to access logs when used (caddyserver#5859)

* fileserver: New --precompressed flag (caddyserver#5880)

exposes the file_server precompressed functionality to be used with the
file-server command

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* fileserver: Enable compression for command by default (caddyserver#5855)

* feat: enable compression for file-server

* refactor

* const

* Update help text

* Update modules/caddyhttp/fileserver/command.go

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* go.mod: Updated quic-go to v0.40.1 (caddyserver#5983)

* metrics: Record request metrics on HTTP errors (caddyserver#5979)

* httpcaddyfile: Sort skip_hosts for deterministic JSON (caddyserver#5990)

* httpcaddyfile: Sort skip_hosts for deterministic JSON

* Update caddyconfig/httpcaddyfile/httptype.go

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* Fix test

* Bah

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* logging: Add `zap.Option` support (caddyserver#5944)

* cmd: use automaxprocs for better perf in containers (caddyserver#5711)

* feat: use automaxprocs for better perf in containers

* better logs

* cs

* build(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (caddyserver#5994)

Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](golang/crypto@v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: Pistasj <hi@pistasjis.net>
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
Signed-off-by: Pascal Vorwerk <info@fossores.de>
Signed-off-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>
Signed-off-by: Mariano Cano <mariano.cano@gmail.com>
Signed-off-by: Dan Lorenc <dlorenc@chainguard.dev>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
Co-authored-by: Jacob Gadikian <jacobgadikian@gmail.com>
Co-authored-by: Shyim <github@shyim.de>
Co-authored-by: Aaron Dewes <aaron@runcitadel.space>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: pistasjis <57069715+pistasjis@users.noreply.github.com>
Co-authored-by: guangwu <guoguangwu@magic-shield.com>
Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Co-authored-by: Karun Agarwal <113603846+singhalkarun@users.noreply.github.com>
Co-authored-by: Marten Seemann <martenseemann@gmail.com>
Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
Co-authored-by: Paul Jeannot <paul.jeannot95@gmail.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Evan Van Dam <evandam92@gmail.com>
Co-authored-by: Pascal Vorwerk <info@fossores.de>
Co-authored-by: glowinthedark <48893368+glowinthedark@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kévin Dunglas <kevin@dunglas.fr>
Co-authored-by: Patrick Koenig <pkoenig10@gmail.com>
Co-authored-by: Thanmay Nath <110758050+ThanmayNath@users.noreply.github.com>
Co-authored-by: Christoph <github@yozora.eu>
Co-authored-by: Fred Cox <mcfedr@gmail.com>
Co-authored-by: Bas Westerbaan <bas@westerbaan.name>
Co-authored-by: Forza <68693597+Forza-tng@users.noreply.github.com>
Co-authored-by: Norman Soetbeer <norman.soetbeer@gmail.com>
Co-authored-by: Harish Shan <140232061+perhapsmaple@users.noreply.github.com>
Co-authored-by: Ethan Brown (Domino) <111539728+ddl-ebrown@users.noreply.github.com>
Co-authored-by: Mariano Cano <mariano.cano@gmail.com>
Co-authored-by: dlorenc <lorenc.d@gmail.com>
Co-authored-by: Andreas Kohn <andreas.kohn@gmail.com>
Co-authored-by: Benjamin Marwell <bmarwell@apache.org>
Co-authored-by: Aziz Rmadi <46684200+armadi1809@users.noreply.github.com>
Co-authored-by: Jens-Uwe Mager <jum@anubis.han.de>
Co-authored-by: David DeMoss <ddemoss222@gmail.com>
Co-authored-by: Tim Geoghegan <timgeog@gmail.com>
Comment on lines +17 to +18
// USE address from PROXY header
PolicyUSE
Copy link

@TimWolla TimWolla Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't do Caddy and I only do the bare minimum of Go. This came to my attention via haproxy/haproxy#2450 and if I understand the code correctly, the USE policy is a footgun will result in security vulnerabilities, as per my comment on the HAProxy issue. My understanding is that it uses the PROXY header if one is sent and does not use it, if it is missing, allowing a malicious client to add the PROXY header itself if a gateway in front of Caddy is trusted, but does not add the PROXY header for whatever reason.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also find REJECT somewhat questionable, because it effectively is SKIP, but worse: It would reject a connection if the first few bytes of the connection happen to match a valid PROXY header by chance, instead of just passing it to the underlying protocol handler.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user knows their Caddy instance is accessible on public networks, they should configure allow to limit PROXY protocol to only those IP ranges.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would reject a connection if the first few bytes of the connection happen to match a valid PROXY header by chance, instead of just passing it to the underlying protocol handler.

We're only concerned with HTTP and TLS in this case, so that's not a concern. We have a different implementation for https://github.com/mholt/caddy-l4 which allows any TCP/UDP traffic.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user knows their Caddy instance is accessible on public networks, they should configure allow to limit PROXY protocol to only those IP ranges.

My understanding is that the allow configuration option sets the USE policy for a given client as per

https://github.com/caddyserver/caddy/pull/5915/files#diff-39cd21a1e121836c0740aa16ed05549fb146c932b9c0f93f5409a6ccb902c0a9R92-R97

Now consider the following:

  1. I configure allow 10.10.10.10 to preserve IP addresses from my trusted gateway running at 10.10.10.10.
  2. This configures the USE policy, which to my understanding takes the client information from the PROXY header if present and accepts the connection without doing anything special if the PROXY header is not present.
  3. My trusted gateway at 10.10.10.10 does not add the PROXY header for whatever reason (bug, configuration mistake, ...).
  4. Now a malicious client could connect to my trusted gateway at 10.10.10.10 and prepend a valid PROXY header to their request. This PROXY header would be forwarded as-is to Caddy and then accepted.

If instead the REQUIRE policy would be configured, I would notice at step (3), because all connections from my trusted gateway would be rejected as the PROXY header is missing, thus alerting my monitoring.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. So the concern is that the "trusted" gateway somehow becomes not trusted? Sounds like it's already game-over at that point. It would be very wrong for a gateway to pass through PROXY protocol bytes as-is without validation/stripping, if it is itself configured to add PROXY protocol.

The problem with REQUIRE is that it's a common for internal apps (from private IP ranges) to need to connect to the server directly (e.g. a cron job which uses the HTTP server to publish an event of somekind, or a metrics scraper pulling data), and those apps may not support adding PROXY protocol; and for those usecases the connecting IP is not relevant anyway.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimWolla Apologies that this turned out so verbose, but I wanted to be thorough 😓

  • I've provided a reproduction environment that you are welcome to show an exploit for due to go-proxyproto policy feature allowing to accept the PROXY protocol header to replace the Client IP only when the host is trusted, but otherwise allowing connections through including from the trusted host.
  • Traefik without any handling of PROXY protocol or trust can route non-TLS connections via TCP router to Caddy, which can trust Traefik as a host IP for PROXY protocol. This is the only situation where the client could forge their IP via PROXY protocol.
  • I have written a program to proxy a TCP connection and inject a PROXY protocol header with fake IP to try exploit, but no success beyond the above generic TCP connection router unaware of PROXY protocol.

as per my comment on the HAProxy issue

allowing a malicious client to add the PROXY header itself if a gateway in front of Caddy is trusted, but does not add the PROXY header for whatever reason.

For others in this discussion to be aware of, I have provided a compose.yaml environment that can be used to easily setup Caddy with a trusted host (Traefik).

While @TimWolla may not be familiar with Caddy, hopefully that is sufficient for them to use for demonstrating how a client could exploit the trust policy feature go-proxyproto has.

The flexibility of trust via policy by not only host IP, but also on the port (by making PROXY header optional for a trusted host) is quite nice to have vs requiring separate ports just to allow trusted hosts to preserve a client IP via PROXY protocol.

The current PROXY protocol specification also disallows normal connections to a port by untrusted IPs (which Caddy 2.7 allowed to configure for). go-proxyproto expands on that with the PROXY header being optional. Personally I don't see a concern with either of these features, so I'd love to see it exploited to clear up any misunderstanding I have.


Now a malicious client could connect to my trusted gateway at 10.10.10.10 and prepend a valid PROXY header to their request. This PROXY header would be forwarded as-is to Caddy and then accepted.

This makes no sense to me. How is the gateway trusted if a malicious client can connect to it with their own PROXY header added?

The information in the PROXY header is limited:

image

From the PROXY protocol spec in question:

  • The receiver MUST be configured to only receive the protocol described in this specification and MUST not try to guess whether the protocol header is present or not.
  • This means that the protocol explicitly prevents port sharing between public and private access.
    Otherwise it would open a major security breach by allowing untrusted parties to spoof their connection addresses.
  • The receiver SHOULD ensure proper access filtering so that only trusted proxies are allowed to use this protocol.

If you trust any client connection from 10.10.10.10 to provide PROXY protocol, then anyone can abuse that. No one disputes that, but has no relevance to optionally allowing connections to not use PROXY protocol to the same receiver port?

The only security concern is that when you receive PROXY protocol header, that it only be trusted if the connecting client is trusted. What's changed here with your example of a malicious client?

Is there something different to this scenario you've described compared to the compose.yaml example I've linked?:

Client => <PROXY protocol not accepted> => Traefik => <Optional PROXY protocol> => Caddy

Only Traefik can add the PROXY protocol header for those connections. I think your concern is where Traefik (or any other service) would unconditionally forward that PROXY header through to Caddy instead of managing that itself as the trusted host.

If that's the case, Traefik is not configured by any means as a PROXY protocol receiver, only Caddy (which only trusts Traefik). The fact that PROXY protocol is optional for connections to Caddy at the same port doesn't change that outcome?

Previous findings (ignore)

I have confirmed by having Caddy make a connection back to Traefik (via the reverse_proxy directive):

  • If Caddy performs that request with PROXY protocol then Traefik rejects it (unless explicitly configuring trust for incoming connections via entrypoints using PROXY protocol).
  • It could also be tested with curl --haproxy-protocol.

For traffic routed through Traefik (via the TCP TLS router without PROXY protocol) to Caddy, no forgery occurs.

  • If Traefik permits a client connecting to an entrypoint to use PROXY protocol, the client IP is resolved at the entrypoint, the PROXY header is not retained to carry over to Caddy (even with TLS passthrough or a non-TLS TCP router that just routes the TCP traffic).

  • If I enable PROXY protocol again between Traefik => Caddy, and have both curl and Caddy perform requests using PROXY protocol, the final response is the client IP belonging to Caddy. That is (with PROXY protocol used between each request =>):

    Client (curl) => Traefik => Caddy (reverse_proxy request) => Traefik => Caddy (response to reverse_proxy request)
    

UPDATE: While exploring this I realized it wasn't sufficient. I had to learn how to write a TCP proxy that injects a PROXY header to my curl requests (tried avoiding curl but at least with rust that complicated getting the response back with my inexperience).

I ran the program outside of the Docker subnet and connected to the exposed ports, where I'm aware of some security gotchas:

  • From the Docker host, accessing the docker subnet will route the traffic with source IP as the network gateway IP. If the trusted hosts happens to be that entire subnet, that's an easy way in through trust.
  • External clients over IPv6 will also be able to route through to a Docker container with published ports that only have an IPv4 network via this same gateway IP.
  • Both are due to Dockers default network management with iptables, they can be avoided by opting out of things like userland-proxy.
  • Those issues provide a simpler way to do such a test with curl --haproxy-protocol --connect-to ::localhost:80 http://example.com to provide the client IP 127.0.0.1 instead of the rewritten client IP Docker introduced before it reaches Traefik. I had forgotten about this until after I wrote the rust program 😓

Ignoring that, when no trust is configured at the Traefik entrypoint, the client can do request forgery.

  • A non-TLS TCP router will just pass the incoming traffic through to the proxied service. This allows the curl client to provide it's own PROXY header and for Caddy to accept it since Traefik IP is trusted.
  • This won't effect HTTP routers that expect HTTP / TLS traffic, likewise a TCP router with TLS (passthrough or not) as the unexpected PROXY protocol header interferes.
  • It also fails when Traefik configures PROXY protocol for a route to the service, as this would add a 2nd PROXY header, causing the service like Caddy which is expecting HTTP or TLS connections (with optional PROXY header) to fail as the 2nd PROXY header remains, causing issues like described for Traefik.
  • If Traefik did configure an explicit set of trusted hosts (even if it was an IP that'd never connect to Traefik), untrusted clients trying to sneak in their own PROXY header would have the header removed and treat it like a normal connection without it (IGNORE policy).
  • For any explicitly trusted hosts providing a PROXY header, Traefik will acknowledge the client IP from the PROXY header (USE policy) and if the TCP routed service enabled PROXY protocol, that will also become the client IP (regardless of the actual IP of the connecting trusted host to Traefik) given to the proxied service, Caddy.

Caddy in 2.7 by default (without configuring any trusted hosts via allow) would have accepted any PROXY protocol header. While with the upcoming 2.8 release with go-proxyproto has the same behaviour I observed with Traefik not having configured trusted hosts. They both default to IGNORE policy (EDIT: Traefik doesn't process the header by default, so IGNORE is not exactly correct).


I also find REJECT somewhat questionable, because it effectively is SKIP, but worse: It would reject a connection if the first few bytes of the connection happen to match a valid PROXY header by chance, instead of just passing it to the underlying protocol handler.

Given findings above, how likely is that to be a practical concern?

In this PR, REJECT is only used for errors prior to having the host IP parsed, then it is checked in the allow and deny lists:

  • If it's in deny list REJECT refuses the connection if it has the PROXY header 👍
  • If it's in the allow list USE does the opposite and trusts the PROXY header for client IP 👍 (REQUIRE adds nothing here, since the expectation is already that the PROXY header is present, optionally allowing connections through without one)
  • If the IP is not in either allow or deny, IGNORE 👍 (discard the header as if it weren't provided, allowing the connection)

Traefik has a very similar policy, but they only have an allow list for USE, otherwise using IGNORE but only if an allow list exists.

If I was proxying connections via Traefik TCP router without TLS, and I knew the proxied service (Caddy) was trusting Traefik for PROXY header, I'd probably want REJECT as a policy for any untrusted client going through that route instead of IGNORE. Since a valid PROXY header by chance would be a known security issue in that scenario?

If instead there was no trusted PROXY protocol hosts to be concerned about, and the service is doing something completely unrelated, then sure REJECT would be bad for that.

REQUIRE only makes sense to enforce if you only ever expect connections to that service port to be traffic that requires the client IP to be corrected. If you're concerned about misconfiguration by the PROXY header not being present, then you could monitor logs for the client IP being the trusted host IP, except when that is worse than service failure from rejecting connections, then prefer REQUIRE. The policy approach is just more granular than IP or necessitating a separate port configuration to support internal + external traffic to a service.

REJECT rejects a connection missing the PROXY header while SKIP allows a connection through without a PROXY header, not sure how that's effectively the same? (see usage in go-proxyproto here), it was implemented for k8s to have local/internal traffic not need to go through a proxy just to get the PROXY header added for the receiving service to accept the connection (where the direct connection provides expected client IP without needing PROXY header like external traffic through a reverse proxy does).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@francislavoie

It would be very wrong for a gateway to pass through PROXY protocol bytes as-is without validation/stripping, if it is itself configured to add PROXY protocol.

No, it would be very wrong for a gateway to touch the PROXY header - unless its client is also expected to send a proxy header. The gateway should just add the PROXY header and not touch anything the client sends, otherwise it would modify the data sent by the client. The target service would then parse (and strip) the (single) PROXY header added by the gateway and attempt to interpret the fake PROXY header by the client as whatever protocol the backend speaks.

That's why the REJECT policy is non-sense.

The problem with REQUIRE is that it's a common for internal apps (from private IP ranges) to need to connect to the server directly (e.g. a cron job which uses the HTTP server to publish an event of somekind, or a metrics scraper pulling data), and those apps may not support adding PROXY protocol; and for those usecases the connecting IP is not relevant anyway.

Then the policy for those internal apps that do not send a PROXY header must be configured as SKIP. Everything else is semantically incorrect and/or a security vulnerability, because something that is not a PROXY header might be interpreted as one.

See also my explanation over in HAProxy's issue tracker: haproxy/haproxy#2450 (comment)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@polarathene

For folks following along, I've provided an extensive response over in HAProxy's tracker: haproxy/haproxy#2450 (comment)

The current PROXY protocol specification also disallows normal connections to a port by untrusted IPs (which Caddy 2.7 allowed to configure for). go-proxyproto expands on that with the PROXY header being optional. Personally I don't see a concern with either of these features, so I'd love to see it exploited to clear up any misunderstanding I have.

I believe the important part is the distinction between “optional” (i.e. USE; unsafe) and “no special handling” (i.e. SKIP; safe). Quoting from my response in HAProxy's tracker:

Conditionally handling which hosts can connect without PROXY procotol is fine, if the decision is made without looking into the contents.
[…]
A trust policy on the same port is fine, if it's a decision between “client MUST send a PROXY header” and “no special handling is performed”. Making the PROXY header optional is not fine. Note the important distinction between “no special handling” and ”optional”.

How is the gateway trusted if a malicious client can connect to it with their own PROXY header added?

see my response in HAProxy's tracker (keyword: dumb TCP forwarder).

A non-TLS TCP router will just pass the incoming traffic through to the proxied service. This allows the curl client to provide it's own PROXY header and for Caddy to accept it since Traefik IP is trusted.

This is the security vulnerability of the USE policy. With the REQUIRE policy, those cases would easily be detected, because the vast majority of traffic is rejected, due to the lack of the PROXY header.

Given findings above, how likely is that to be a practical concern? […] If it's in deny list REJECT refuses the connection if it has the PROXY header 👍

re practical concern: Depends on your type of traffic. For HTTP it is likely not a concern, because a PROXY header is not valid HTTP and thus the request would be rejected no-matter-what.

If it's in the allow list USE does the opposite and trusts the PROXY header for client IP 👍 (REQUIRE adds nothing here, since the expectation is already that the PROXY header is present, optionally allowing connections through without one)

At the risk of repeating myself: USE is unsafe. Only REQUIRE is safe.

If the IP is not in either allow or deny, IGNORE 👍 (discard the header as if it weren't provided, allowing the connection)

See REJECT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dependencies ⛓️ Pull requests that update a dependency file
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants