Compare commits

..

7 Commits

Author SHA1 Message Date
Adrien PONSIN
59b95ea315
improve default regex 2025-04-17 21:46:41 +02:00
Adrien PONSIN
fcaafbf3f4
still working? 2025-04-17 21:16:07 +02:00
Adrien PONSIN
c55dec6bc4
use a sync.Pool to reuse connexions 2025-04-17 18:41:32 +02:00
Adrien PONSIN
17aa156fa5
try to improve 2025-04-17 17:56:39 +02:00
Adrien PONSIN
7bc9fa242e
add an error log for unauthorized container 2025-04-17 17:15:58 +02:00
Adrien PONSIN
3937b7cda5
try fixing logic 2025-04-17 17:11:00 +02:00
Adrien PONSIN
52b4d44b4b
remove comments and add the container name 2025-04-17 15:19:19 +02:00
2 changed files with 85 additions and 68 deletions

View File

@ -6,15 +6,16 @@ import (
"fmt" "fmt"
"gitea.illuad.fr/adrien/middleman" "gitea.illuad.fr/adrien/middleman"
"gitea.illuad.fr/adrien/middleman/flag" "gitea.illuad.fr/adrien/middleman/flag"
"gitea.illuad.fr/adrien/middleman/pkg/fastrp"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"net" "net"
"net/http" "net/http"
"net/http/httputil"
"net/netip" "net/netip"
"net/url" "net/url"
"regexp" "regexp"
"slices"
"strings" "strings"
"time" "time"
) )
@ -34,25 +35,7 @@ type methodRegex = map[string]*regexp.Regexp
// ProxyHandler takes an incoming request and sends it to another server, proxying the response back to the client. // ProxyHandler takes an incoming request and sends it to another server, proxying the response back to the client.
type ProxyHandler struct { type ProxyHandler struct {
rp *httputil.ReverseProxy frp *fastrp.FastRP
}
// ErrHTTPMethodNotAllowed is returned if the request's HTTP method is not allowed for this container.
type ErrHTTPMethodNotAllowed struct {
httpMethod string
}
// ErrNoMatch is returned if the request's URL path is not allowed for this container.
type ErrNoMatch struct {
path, httpMethod string
}
func (e ErrHTTPMethodNotAllowed) Error() string {
return fmt.Sprintf("%s is not in the list of HTTP methods allowed this container", e.httpMethod)
}
func (e ErrNoMatch) Error() string {
return fmt.Sprintf("%s does not match any registered regular expression for this HTTP method (%s)", e.path, e.httpMethod)
} }
// ServeCmd is the command name. // ServeCmd is the command name.
@ -92,8 +75,8 @@ const (
defaultHealthEndpoint = "/healthz" defaultHealthEndpoint = "/healthz"
// defaultHealthListenAddr is the proxy health endpoint default listen address and port. // defaultHealthListenAddr is the proxy health endpoint default listen address and port.
defaultHealthListenAddr = "127.0.0.1:5732" defaultHealthListenAddr = "127.0.0.1:5732"
// defaultAllowedRequest is the proxy default allowed request. // defaultAllowedRequests is the default allowed requests. Note that they should be sufficient to make Traefik work properly.
defaultAllowedRequest = "^/(version|containers/.*|events.*)$" defaultAllowedRequests = "/v1.\\d{1,2}/(version|containers/(?:json|[a-zA-Z0-9]{64}/json)|events)$"
) )
var s serve var s serve
@ -101,7 +84,7 @@ var s serve
var ( var (
containerMethodRegex = map[string]methodRegex{ containerMethodRegex = map[string]methodRegex{
"*": { "*": {
http.MethodGet: regexp.MustCompile(defaultAllowedRequest), http.MethodGet: regexp.MustCompile(defaultAllowedRequests),
}, },
} }
applicatorURLRegex = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9_.-]+)\(((?:(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS)(?:,(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS))*)?)\):(.*)$`) applicatorURLRegex = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9_.-]+)\(((?:(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS)(?:,(?:GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|TRACE|OPTIONS))*)?)\):(.*)$`)
@ -149,38 +132,40 @@ func Serve(group *errgroup.Group) *cli.Command {
func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug().Str("remote_addr", r.RemoteAddr).Str("method", r.Method).Str("path", r.URL.Path).Msg("incoming request") log.Debug().Str("remote_addr", r.RemoteAddr).Str("method", r.Method).Str("path", r.URL.Path).Msg("incoming request")
mr, ok := containerMethodRegex["*"] if mr, ok := containerMethodRegex["*"]; ok {
if ok { if code := ph.checkMethodAndRegex(mr, r, ""); code != http.StatusOK {
if ph.checkMethodAndRegex(r, mr) { http.Error(w, http.StatusText(code), code)
ph.rp.ServeHTTP(w, r) return
}
ph.frp.ServeHTTP(w, r)
return
}
host, _, _ := net.SplitHostPort(r.RemoteAddr)
for containerName, mr := range containerMethodRegex {
if ph.isContainerAuthorized(containerName, host) {
if code := ph.checkMethodAndRegex(mr, r, containerName); code != http.StatusOK {
http.Error(w, http.StatusText(code), code)
return
}
ph.frp.ServeHTTP(w, r)
return return
} }
} }
var ( logDeniedRequest(r, http.StatusUnauthorized, "this container is not on the list of authorized ones")
containerName string http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
authorized bool }
host, _, _ = net.SplitHostPort(r.RemoteAddr)
) func (ph *ProxyHandler) isContainerAuthorized(containerName, host string) bool {
for containerName, mr = range containerMethodRegex { resolvedIPs, err := net.LookupIP(containerName)
resolvedIPs, err := net.LookupIP(containerName) if err != nil {
if err != nil { return false
continue }
} for resolvedIP := range slices.Values(resolvedIPs) {
for _, resolvedIP := range resolvedIPs { if resolvedIP.Equal(net.ParseIP(host)) {
if resolvedIP.Equal(net.ParseIP(host)) { return true
if ph.checkMethodAndRegex(r, mr) {
authorized = true
break
}
}
} }
} }
if !authorized { return false
logDeniedRequest(r, http.StatusUnauthorized, "this container is not on the list of authorized ones")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
ph.rp.ServeHTTP(w, r)
} }
func logDeniedRequest(r *http.Request, statusCode int, message string) { func logDeniedRequest(r *http.Request, statusCode int, message string) {
@ -203,23 +188,18 @@ func logAuthorizedRequest(r *http.Request, containerName, message string) {
l.Msg(message) l.Msg(message)
} }
func (ph *ProxyHandler) checkMethodAndRegex(r *http.Request, mr methodRegex) bool { func (ph *ProxyHandler) checkMethodAndRegex(mr methodRegex, r *http.Request, containerName string) int {
req, ok := mr[r.Method] req, ok := mr[r.Method]
if !ok { if !ok {
logDeniedRequest(r, http.StatusMethodNotAllowed, "this HTTP method is not in the list of those authorized for this container") logDeniedRequest(r, http.StatusMethodNotAllowed, "this HTTP method is not in the list of those authorized for this container")
return false return http.StatusMethodNotAllowed
// http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
// return
} }
if !req.MatchString(r.URL.Path) { if !req.MatchString(r.URL.Path) {
logDeniedRequest(r, http.StatusForbidden, "this path does not match any regular expression for this HTTP method") logDeniedRequest(r, http.StatusForbidden, "this path does not match any regular expression for this HTTP method")
return false return http.StatusForbidden
// http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
// return
} }
logAuthorizedRequest(r, "", "incoming request matches a registered regular expression") logAuthorizedRequest(r, containerName, "incoming request matches a registered regular expression")
return true return http.StatusOK
// ph.rp.ServeHTTP(w, r)
} }
// action is executed when the ServeCmd command is called. // action is executed when the ServeCmd command is called.
@ -237,14 +217,10 @@ func (s serve) action(ctx context.Context, command *cli.Command) error {
return err return err
} }
dummyURL, _ := url.Parse("http://dummy") dummyURL, _ := url.Parse("http://dummy")
rp := httputil.NewSingleHostReverseProxy(dummyURL)
rp.Transport = &http.Transport{
DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) {
return net.Dial("unix", command.String(dockerSocketPathFlagName))
},
}
srv := &http.Server{ // #nosec: G112 srv := &http.Server{ // #nosec: G112
Handler: &ProxyHandler{rp: rp}, Handler: &ProxyHandler{
frp: fastrp.NewRP("unix", command.String(dockerSocketPathFlagName), dummyURL),
},
} }
s.group.Go(func() error { s.group.Go(func() error {
if err = srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) { if err = srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) {
@ -443,7 +419,7 @@ func addRequests() cli.Flag {
Usage: "add requests", Usage: "add requests",
Sources: middleman.PluckEnvVar(EnvVarPrefix, addRequestsFlagName), Sources: middleman.PluckEnvVar(EnvVarPrefix, addRequestsFlagName),
// Local: true, // Required to trigger the Action when this flag is set via the environment variable, see https://github.com/urfave/cli/issues/2041. // Local: true, // Required to trigger the Action when this flag is set via the environment variable, see https://github.com/urfave/cli/issues/2041.
Value: []string{"*:" + defaultAllowedRequest}, Value: []string{"*:" + defaultAllowedRequests},
Aliases: middleman.PluckAlias(ServeCmd, addRequestsFlagName), Aliases: middleman.PluckAlias(ServeCmd, addRequestsFlagName),
Action: func(ctx context.Context, command *cli.Command, requests []string) error { Action: func(ctx context.Context, command *cli.Command, requests []string) error {
clear(containerMethodRegex) clear(containerMethodRegex)

41
pkg/fastrp/fastrp.go Normal file
View File

@ -0,0 +1,41 @@
package fastrp
import (
"context"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
)
type FastRP struct {
rp *httputil.ReverseProxy
connPool *sync.Pool
}
func NewRP(network, address string, url *url.URL) *FastRP {
roundTripper := &http.Transport{
DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) {
return net.Dial(network, address)
},
MaxIdleConns: 10,
}
frp := &FastRP{
rp: httputil.NewSingleHostReverseProxy(url),
connPool: &sync.Pool{
New: func() any {
return roundTripper.Clone()
},
},
}
frp.rp.Transport = frp.connPool.Get().(http.RoundTripper)
return frp
}
func (frp *FastRP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
roundTripper := frp.connPool.Get().(http.RoundTripper)
defer frp.connPool.Put(roundTripper)
frp.rp.Transport = roundTripper
frp.rp.ServeHTTP(w, r)
}