[#371] Add custom Source IP header configuration

Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
This commit is contained in:
Marina Biryukova 2024-04-17 17:08:55 +03:00
parent e22ff52165
commit e3f03f4599
11 changed files with 54 additions and 13 deletions

View file

@ -30,6 +30,7 @@ This document outlines major changes between releases.
- Add new `reconnect_interval` config param (#291)
- Support `GetBucketPolicyStatus` (#301)
- Add FrostfsID cache (#269)
- Add new `source_ip_header` config param (#371)
### Changed
- Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221)

View file

@ -415,7 +415,7 @@ func prepareTestRequestWithQuery(hc *handlerContext, bktName, objName string, qu
r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body))
r.URL.RawQuery = query.Encode()
reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName})
reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "")
r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo))
return w, r
@ -425,7 +425,7 @@ func prepareTestPayloadRequest(hc *handlerContext, bktName, objName string, payl
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultURL, payload)
reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName})
reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "")
r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo))
return w, r

View file

@ -315,7 +315,7 @@ func TestPutBucketLockConfigurationHandler(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body))
r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket})))
r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}, "")))
hc.Handler().PutBucketObjectLockConfigHandler(w, r)
@ -388,7 +388,7 @@ func TestGetBucketLockConfigurationHandler(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(nil))
r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket})))
r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}, "")))
hc.Handler().GetBucketObjectLockConfigHandler(w, r)

View file

@ -351,7 +351,7 @@ func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName strin
req.Body = io.NopCloser(reqBody)
w := httptest.NewRecorder()
reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName})
reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "")
req = req.WithContext(middleware.SetReqInfo(ctx, reqInfo))
req = req.WithContext(middleware.SetBox(req.Context(), &middleware.Box{
ClientTime: signTime,

View file

@ -83,17 +83,24 @@ var (
)
// NewReqInfo returns new ReqInfo based on parameters.
func NewReqInfo(w http.ResponseWriter, r *http.Request, req ObjectRequest) *ReqInfo {
return &ReqInfo{
func NewReqInfo(w http.ResponseWriter, r *http.Request, req ObjectRequest, sourceIPHeader string) *ReqInfo {
reqInfo := &ReqInfo{
API: req.Method,
BucketName: req.Bucket,
ObjectName: req.Object,
UserAgent: r.UserAgent(),
RemoteHost: getSourceIP(r),
RequestID: GetRequestID(w),
DeploymentID: deploymentID.String(),
URL: r.URL,
}
if sourceIPHeader != "" {
reqInfo.RemoteHost = r.Header.Get(sourceIPHeader)
} else {
reqInfo.RemoteHost = getSourceIP(r)
}
return reqInfo
}
// AppendTags -- appends key/val to ReqInfo.tags.
@ -193,6 +200,7 @@ func GetReqLog(ctx context.Context) *zap.Logger {
type RequestSettings interface {
NamespaceHeader() string
ResolveNamespaceAlias(string) string
SourceIPHeader() string
}
func Request(log *zap.Logger, settings RequestSettings) Func {
@ -211,7 +219,7 @@ func Request(log *zap.Logger, settings RequestSettings) Func {
// set request info into context
// bucket name and object will be set in reqInfo later (limitation of go-chi)
reqInfo := NewReqInfo(w, r, ObjectRequest{})
reqInfo := NewReqInfo(w, r, ObjectRequest{}, settings.SourceIPHeader())
reqInfo.Namespace = settings.ResolveNamespaceAlias(r.Header.Get(settings.NamespaceHeader()))
r = r.WithContext(SetReqInfo(r.Context(), reqInfo))
@ -317,11 +325,14 @@ func getSourceIP(r *http.Request) string {
}
}
if addr != "" {
return addr
if addr == "" {
addr = r.RemoteAddr
}
// Default to remote address if headers not set.
addr, _, _ = net.SplitHostPort(r.RemoteAddr)
return addr
raddr, _, _ := net.SplitHostPort(addr)
if raddr == "" {
return addr
}
return raddr
}

View file

@ -64,6 +64,10 @@ type middlewareSettingsMock struct {
aclEnabled bool
}
func (r *middlewareSettingsMock) SourceIPHeader() string {
return ""
}
func (r *middlewareSettingsMock) NamespaceHeader() string {
return FrostfsNamespaceHeader
}

View file

@ -109,6 +109,7 @@ type (
defaultNamespaces []string
authorizedControlAPIKeys [][]byte
policyDenyByDefault bool
sourceIPHeader string
}
maxClientsConfig struct {
@ -235,6 +236,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger, key *keys.PrivateK
s.setMD5Enabled(v.GetBool(cfgMD5Enabled))
s.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(log, v), key.PublicKey()))
s.setPolicyDenyByDefault(v.GetBool(cfgPolicyDenyByDefault))
s.setSourceIPHeader(v.GetString(cfgSourceIPHeader))
}
func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) {
@ -429,6 +431,18 @@ func (s *appSettings) setPolicyDenyByDefault(policyDenyByDefault bool) {
s.mu.Unlock()
}
func (s *appSettings) setSourceIPHeader(header string) {
s.mu.Lock()
s.sourceIPHeader = header
s.mu.Unlock()
}
func (s *appSettings) SourceIPHeader() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.sourceIPHeader
}
func (a *App) initAPI(ctx context.Context) {
a.initLayer(ctx)
a.initHandler()

View file

@ -181,6 +181,8 @@ const ( // Settings.
// Namespaces.
cfgNamespacesConfig = "namespaces.config"
cfgSourceIPHeader = "source_ip_header"
// Command line args.
cmdHelp = "help"
cmdVersion = "version"

View file

@ -220,3 +220,6 @@ S3_GW_PROXY_CONTRACT=proxy.frostfs
# Namespaces configuration
S3_GW_NAMESPACES_CONFIG=namespaces.json
# Custom header to retrieve Source IP
S3_GW_SOURCE_IP_HEADER=Source-Ip

View file

@ -259,3 +259,6 @@ proxy:
namespaces:
config: namespaces.json
# Custom header to retrieve Source IP
source_ip_header: "Source-Ip"

View file

@ -220,6 +220,8 @@ allowed_access_key_id_prefixes:
- 3stjWenX15YwYzczMr88gy3CQr4NYFBQ8P7keGzH5QFn
reconnect_interval: 1m
source_ip_header: "Source-Ip"
```
| Parameter | Type | SIGHUP reload | Default value | Description |
@ -236,6 +238,7 @@ reconnect_interval: 1m
| `max_clients_deadline` | `duration` | no | `30s` | Deadline after which the gate sends error `RequestTimeout` to a client. |
| `allowed_access_key_id_prefixes` | `[]string` | no | | List of allowed `AccessKeyID` prefixes which S3 GW serve. If the parameter is omitted, all `AccessKeyID` will be accepted. |
| `reconnect_interval` | `duration` | no | `1m` | Listeners reconnection interval. |
| `source_ip_header` | `string` | yes | | Custom header to retrieve Source IP. |
### `wallet` section