mirror of
https://github.com/fosrl/badger.git
synced 2026-02-08 05:56:46 +00:00
support pulling real ip from proxy
This commit is contained in:
88
README.md
88
README.md
@@ -1,18 +1,23 @@
|
||||
# Badger Plugin for Traefik with Pangolin Integration
|
||||
# Pangolin Middleware: Badger
|
||||
|
||||
Badger is a middleware plugin designed to work with the Traefik reverse proxy in conjunction with [Pangolin](https://github.com/fosrl/pangolin), a multi-tenant tunneled reverse proxy server and management interface with identity and access management. Badger acts as an authentication bouncer, ensuring only authenticated and authorized requests are allowed through the proxy.
|
||||
Badger is a middleware plugin designed to work with the Traefik reverse proxy in conjunction with [Pangolin](https://github.com/fosrl/pangolin), an identity-aware reverse proxy and VPN. Badger acts as an authentication bouncer, ensuring only authenticated and authorized requests are allowed through the proxy.
|
||||
|
||||
This plugin is **required** to be configured alongside [Pangolin](https://github.com/fosrl/pangolin) to enforce secure authentication and session management.
|
||||
> [!NOTE]
|
||||
> Badger can also be used standalone for IP handling (Cloudflare and custom proxy support) without Pangolin. Simply set `disableForwardAuth: true` in your configuration. See the [Disabling Forward Auth](#disabling-forward-auth) section below for details.
|
||||
|
||||
This plugin is **required** to be installed alongside [Pangolin](https://github.com/fosrl/pangolin) to enforce secure authentication and session management.
|
||||
|
||||
## Installation
|
||||
|
||||
Learn how to set up [Pangolin](https://github.com/fosrl/pangolin) and Badger in the [Pangolin Documentation](https://github.com/fosrl/pangolin).
|
||||
Badger is automatically installed with Pangolin. Learn how to install Pangolin in the [Pangolin Documentation](https://docs.pangolin.net/self-host/quick-install).
|
||||
|
||||
## Configuration
|
||||
|
||||
Badger requires the following configuration parameters to be specified in your [Traefik configuration file](https://doc.traefik.io/traefik/getting-started/configuration-overview/). These coincide with the separate [Pangolin](https://github.com/fosrl/pangolin) configuration file. These options do not need to be configured unless you need to override the automatically provided config from Pangolin.
|
||||
Pangolin will provide the necessary configuration to Badger automatically via the HTTP provider. However, you can override the configuration settings by manually providing them in the Traefik config.
|
||||
|
||||
### Configuration Options
|
||||
### Required Configuration Options
|
||||
|
||||
When forward auth is enabled (default), the following options are required:
|
||||
|
||||
```yaml
|
||||
apiBaseUrl: "http://localhost:3001/api/v1"
|
||||
@@ -20,6 +25,77 @@ userSessionCookieName: "p_session_token"
|
||||
resourceSessionRequestParam: "p_session_request"
|
||||
```
|
||||
|
||||
### Disabling Forward Auth
|
||||
|
||||
To disable forward auth and only use IP handling, set `disableForwardAuth: true`. When enabled, all requests pass through without authentication, and the required configuration options above are not needed:
|
||||
|
||||
```yaml
|
||||
disableForwardAuth: true
|
||||
|
||||
# IP handling configuration (optional)
|
||||
trustip:
|
||||
- "10.0.0.0/8"
|
||||
customIPHeader: "X-Forwarded-For"
|
||||
```
|
||||
|
||||
### IP Handling Configuration
|
||||
|
||||
Badger automatically extracts the real client IP from requests. By default, it trusts Cloudflare IP ranges and uses the `CF-Connecting-IP` header.
|
||||
|
||||
#### Using with Cloudflare (Default)
|
||||
|
||||
No additional configuration needed. Badger automatically:
|
||||
|
||||
- Trusts Cloudflare IP ranges
|
||||
- Extracts IP from `CF-Connecting-IP` header
|
||||
- Sets `X-Real-IP` and `X-Forwarded-For` headers for downstream services
|
||||
|
||||
#### Using without Cloudflare
|
||||
|
||||
If you're using a different proxy or load balancer, configure custom trusted IPs and/or a custom IP header:
|
||||
|
||||
```yaml
|
||||
apiBaseUrl: "http://localhost:3001/api/v1"
|
||||
userSessionCookieName: "p_session_token"
|
||||
resourceSessionRequestParam: "p_session_request"
|
||||
|
||||
# Disable Cloudflare IP ranges
|
||||
disableDefaultCFIPs: true
|
||||
|
||||
# Add your proxy/load balancer IP ranges (CIDR format)
|
||||
trustip:
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
|
||||
# Optional: Use a custom header instead of CF-Connecting-IP
|
||||
customIPHeader: "X-Forwarded-For"
|
||||
```
|
||||
|
||||
### Configuration Options Reference
|
||||
|
||||
| Option | Type | Required* | Default | Description |
|
||||
| ----------------------------- | -------- | --------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| `disableForwardAuth` | bool | No | `false` | Disable forward auth; only IP handling is performed |
|
||||
| `apiBaseUrl` | string | Yes* | - | Base URL of the Pangolin API |
|
||||
| `userSessionCookieName` | string | Yes* | - | Cookie name for user sessions |
|
||||
| `resourceSessionRequestParam` | string | Yes* | - | Query parameter name for resource session requests |
|
||||
| `trustip` | []string | No | `[]` | Array of trusted IP ranges in CIDR format |
|
||||
| `disableDefaultCFIPs` | bool | No | `false` | Disable default Cloudflare IP ranges |
|
||||
| `customIPHeader` | string | No | `""` | Custom header name to extract IP from (only used if request is from trusted source) |
|
||||
|
||||
\* Required only when `disableForwardAuth` is `false` (default)
|
||||
|
||||
|
||||
## Updating Cloudflare IPs
|
||||
|
||||
To update the Cloudflare IP ranges, run:
|
||||
|
||||
```bash
|
||||
./updateCFIps.sh
|
||||
```
|
||||
|
||||
This fetches the latest IP ranges from Cloudflare and updates `ips/ips.go`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
28
ips/ips.go
Normal file
28
ips/ips.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package ips
|
||||
|
||||
func CFIPs() []string {
|
||||
return []string{
|
||||
"173.245.48.0/20",
|
||||
"103.21.244.0/22",
|
||||
"103.22.200.0/22",
|
||||
"103.31.4.0/22",
|
||||
"141.101.64.0/18",
|
||||
"108.162.192.0/18",
|
||||
"190.93.240.0/20",
|
||||
"188.114.96.0/20",
|
||||
"197.234.240.0/22",
|
||||
"198.41.128.0/17",
|
||||
"162.158.0.0/15",
|
||||
"104.16.0.0/13",
|
||||
"104.24.0.0/14",
|
||||
"172.64.0.0/13",
|
||||
"131.0.72.0/22",
|
||||
"2400:cb00::/32",
|
||||
"2606:4700::/32",
|
||||
"2803:f800::/32",
|
||||
"2405:b500::/32",
|
||||
"2405:8100::/32",
|
||||
"2a06:98c0::/29",
|
||||
"2c0f:f248::/32",
|
||||
}
|
||||
}
|
||||
147
main.go
147
main.go
@@ -5,22 +5,40 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fosrl/badger/ips"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
APIBaseUrl string `json:"apiBaseUrl"`
|
||||
UserSessionCookieName string `json:"userSessionCookieName"`
|
||||
ResourceSessionRequestParam string `json:"resourceSessionRequestParam"`
|
||||
APIBaseUrl string `json:"apiBaseUrl,omitempty"`
|
||||
UserSessionCookieName string `json:"userSessionCookieName,omitempty"`
|
||||
ResourceSessionRequestParam string `json:"resourceSessionRequestParam,omitempty"`
|
||||
DisableForwardAuth bool `json:"disableForwardAuth,omitempty"`
|
||||
TrustIP []string `json:"trustip,omitempty"`
|
||||
DisableDefaultCFIPs bool `json:"disableDefaultCFIPs,omitempty"`
|
||||
CustomIPHeader string `json:"customIPHeader,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
xRealIP = "X-Real-Ip"
|
||||
xForwardFor = "X-Forwarded-For"
|
||||
xForwardProto = "X-Forwarded-Proto"
|
||||
cfConnectingIP = "CF-Connecting-IP"
|
||||
cfVisitor = "CF-Visitor"
|
||||
)
|
||||
|
||||
type Badger struct {
|
||||
next http.Handler
|
||||
name string
|
||||
apiBaseUrl string
|
||||
userSessionCookieName string
|
||||
resourceSessionRequestParam string
|
||||
disableForwardAuth bool
|
||||
trustIP []*net.IPNet
|
||||
customIPHeader string
|
||||
}
|
||||
|
||||
type VerifyBody struct {
|
||||
@@ -68,16 +86,61 @@ func CreateConfig() *Config {
|
||||
}
|
||||
|
||||
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
|
||||
return &Badger{
|
||||
badger := &Badger{
|
||||
next: next,
|
||||
name: name,
|
||||
apiBaseUrl: config.APIBaseUrl,
|
||||
userSessionCookieName: config.UserSessionCookieName,
|
||||
resourceSessionRequestParam: config.ResourceSessionRequestParam,
|
||||
}, nil
|
||||
disableForwardAuth: config.DisableForwardAuth,
|
||||
customIPHeader: config.CustomIPHeader,
|
||||
}
|
||||
|
||||
// Validate required fields only if forward auth is enabled
|
||||
if !config.DisableForwardAuth {
|
||||
if config.APIBaseUrl == "" {
|
||||
return nil, fmt.Errorf("apiBaseUrl is required when forward auth is enabled")
|
||||
}
|
||||
if config.UserSessionCookieName == "" {
|
||||
return nil, fmt.Errorf("userSessionCookieName is required when forward auth is enabled")
|
||||
}
|
||||
if config.ResourceSessionRequestParam == "" {
|
||||
return nil, fmt.Errorf("resourceSessionRequestParam is required when forward auth is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
if config.TrustIP != nil {
|
||||
for _, v := range config.TrustIP {
|
||||
_, trustip, err := net.ParseCIDR(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
badger.trustIP = append(badger.trustIP, trustip)
|
||||
}
|
||||
}
|
||||
|
||||
if !config.DisableDefaultCFIPs {
|
||||
for _, v := range ips.CFIPs() {
|
||||
_, trustip, err := net.ParseCIDR(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
badger.trustIP = append(badger.trustIP, trustip)
|
||||
}
|
||||
}
|
||||
|
||||
return badger, nil
|
||||
}
|
||||
|
||||
func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
realIP := p.getRealIP(req)
|
||||
p.setIPHeaders(req, realIP)
|
||||
|
||||
if p.disableForwardAuth {
|
||||
p.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
cookies := p.extractCookies(req)
|
||||
|
||||
queryValues := req.URL.Query()
|
||||
@@ -86,7 +149,7 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
body := ExchangeSessionBody{
|
||||
RequestToken: &sessionRequestValue,
|
||||
RequestHost: &req.Host,
|
||||
RequestIP: &req.RemoteAddr,
|
||||
RequestIP: &realIP,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(body)
|
||||
@@ -162,7 +225,7 @@ func (p *Badger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
RequestPath: &req.URL.Path,
|
||||
RequestMethod: &req.Method,
|
||||
TLS: req.TLS != nil,
|
||||
RequestIP: &req.RemoteAddr,
|
||||
RequestIP: &realIP,
|
||||
Headers: headers,
|
||||
Query: queryParams,
|
||||
}
|
||||
@@ -318,3 +381,73 @@ func (p *Badger) renderRedirectPage(redirectURL string) string {
|
||||
</body>
|
||||
</html>`, redirectURL, redirectURL)
|
||||
}
|
||||
|
||||
func (p *Badger) getRealIP(req *http.Request) string {
|
||||
// Check if request comes from a trusted source
|
||||
isTrusted := p.isTrustedIP(req.RemoteAddr)
|
||||
|
||||
// If custom IP header is configured, use it
|
||||
if p.customIPHeader != "" {
|
||||
if customIP := req.Header.Get(p.customIPHeader); customIP != "" && isTrusted {
|
||||
return customIP
|
||||
}
|
||||
}
|
||||
|
||||
// Default: use CF-Connecting-IP if from trusted source
|
||||
if isTrusted {
|
||||
if cfIP := req.Header.Get(cfConnectingIP); cfIP != "" {
|
||||
return cfIP
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: extract IP from RemoteAddr
|
||||
ip, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
// If parsing fails, return RemoteAddr as-is (might be just IP without port)
|
||||
return req.RemoteAddr
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func (p *Badger) isTrustedIP(remoteAddr string) bool {
|
||||
ipStr, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
for _, network := range p.trustIP {
|
||||
if network.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Badger) setIPHeaders(req *http.Request, realIP string) {
|
||||
isTrusted := p.isTrustedIP(req.RemoteAddr)
|
||||
|
||||
if isTrusted {
|
||||
// Handle CF-Visitor header for scheme
|
||||
if req.Header.Get(cfVisitor) != "" {
|
||||
var cfVisitorValue struct {
|
||||
Scheme string `json:"scheme"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(req.Header.Get(cfVisitor)), &cfVisitorValue); err == nil {
|
||||
req.Header.Set(xForwardProto, cfVisitorValue.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
// Set headers with the real IP (already extracted from CF-Connecting-IP or custom header)
|
||||
req.Header.Set(xForwardFor, realIP)
|
||||
req.Header.Set(xRealIP, realIP)
|
||||
} else {
|
||||
// Not from trusted source, use direct IP
|
||||
req.Header.Set(xRealIP, realIP)
|
||||
// Remove CF headers if present
|
||||
req.Header.Del(cfVisitor)
|
||||
req.Header.Del(cfConnectingIP)
|
||||
}
|
||||
}
|
||||
|
||||
56
updateCFIps.sh
Executable file
56
updateCFIps.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
rm CFIPs.txt
|
||||
curl https://www.cloudflare.com/ips-v4 >>CFIPs.txt
|
||||
echo "" >>CFIPs.txt
|
||||
curl https://www.cloudflare.com/ips-v6 >>CFIPs.txt
|
||||
echo "" >>CFIPs.txt
|
||||
|
||||
OUTPUT_GO_CONFIG="./ips/ips.go"
|
||||
OUTPUT_GO_CONFIG_OLD="./ips-temp.go"
|
||||
|
||||
mv $OUTPUT_GO_CONFIG $OUTPUT_GO_CONFIG_OLD
|
||||
|
||||
echo "// Package ips contains a list of current cloud flare IP ranges" >>$OUTPUT_GO_CONFIG
|
||||
echo "package ips" >>$OUTPUT_GO_CONFIG
|
||||
echo "" >>$OUTPUT_GO_CONFIG
|
||||
echo "// CFIPs is the CloudFlare Server IP list (this is checked on build)." >>$OUTPUT_GO_CONFIG
|
||||
echo "func CFIPs() []string {" >>$OUTPUT_GO_CONFIG
|
||||
echo " return []string{" >>$OUTPUT_GO_CONFIG
|
||||
|
||||
cat CFIPs.txt | while read line || [[ -n $line ]]; do
|
||||
printf '%s\n' "CF IP: $line"
|
||||
echo " \"${line}\"," >>$OUTPUT_GO_CONFIG
|
||||
done
|
||||
|
||||
echo " }" >>$OUTPUT_GO_CONFIG
|
||||
echo "}" >>$OUTPUT_GO_CONFIG
|
||||
|
||||
rm CFIPs.txt
|
||||
|
||||
if [ "${1}" == "pc" ]; then
|
||||
echo "Run on pre-commit hook."
|
||||
if cmp --silent -- "$OUTPUT_GO_CONFIG" "$OUTPUT_GO_CONFIG_OLD"; then
|
||||
echo "No changes, nothing to worry about"
|
||||
else
|
||||
echo "Cloud flare have changed their IPs, adding changes to commit."
|
||||
touch ./.commit
|
||||
fi
|
||||
|
||||
rm $OUTPUT_GO_CONFIG_OLD
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ "${1}" != "ci" ]; then
|
||||
echo "Not run on CI, exit ok"
|
||||
rm $OUTPUT_GO_CONFIG_OLD
|
||||
exit
|
||||
fi
|
||||
|
||||
if cmp --silent -- "$OUTPUT_GO_CONFIG" "$OUTPUT_GO_CONFIG_OLD"; then
|
||||
echo "No changes to Cloud Flare IP list"
|
||||
rm $OUTPUT_GO_CONFIG_OLD
|
||||
else
|
||||
echo "Cloud flare have changed their IPs, re-run updateCFIps.sh and commit the changes!"
|
||||
rm $OUTPUT_GO_CONFIG_OLD
|
||||
exit 6
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user