From a735b9cc5e12649ff07e95d13e6d726649a9e497 Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 10 Feb 2021 23:54:31 +0100 Subject: [PATCH] added auth method --- go.mod | 1 + go.sum | 2 + pkg/alloc/alloc.go | 112 ++++++++++++++++++++++++++++++++++ pkg/api/v1/server/auth.go | 113 +++++++++++++++++++++++++++++++++++ pkg/api/v1/server/handler.go | 5 ++ pkg/api/v1/server/routes.go | 15 +++-- pkg/api/v1/server/server.go | 2 +- pkg/config/config.go | 32 ++++++++-- 8 files changed, 270 insertions(+), 12 deletions(-) create mode 100644 pkg/alloc/alloc.go create mode 100644 pkg/api/v1/server/auth.go diff --git a/go.mod b/go.mod index 024bce2..f9bf906 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/golang/protobuf v1.4.3 // indirect github.com/google/go-cmp v0.5.0 // indirect github.com/google/uuid v1.1.2 // indirect + github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-hclog v0.15.0 github.com/hashicorp/raft v1.2.0 github.com/mdlayher/arp v0.0.0-20191213142603-f72070a231fc diff --git a/go.sum b/go.sum index a04b0cb..6fa3992 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.15.0 h1:qMuK0wxsoW4D0ddCCYwPSTm4KQv1X1ke3WmPWZ0Mvsk= diff --git a/pkg/alloc/alloc.go b/pkg/alloc/alloc.go new file mode 100644 index 0000000..cd3f6dc --- /dev/null +++ b/pkg/alloc/alloc.go @@ -0,0 +1,112 @@ +package alloc + +import ( + "context" + "fmt" + "net" + + "git.giftfish.de/ston1th/haproxy-lb/pkg/cluster" + "github.com/mikioh/ipaddr" +) + +type Alloc struct { + kv cluster.KV + pool []*net.IPNet + gateway *net.IPNet +} + +func NewAlloc(kv cluster.KV, cidrs []string, gateway string) (*Alloc, error) { + pool, err := parseRange(cidrs) + if err != nil { + return nil, err + } + _, gw, err := net.ParseCIDR(gateway) + if err != nil { + return nil, err + } + return &Alloc{kv, pool, gw}, nil +} + +func (a *Alloc) AllocIP(ctx context.Context, name string) (addr string, err error) { + for _, cidr := range a.pool { + c := ipaddr.NewCursor([]ipaddr.Prefix{*ipaddr.NewPrefix(a.gateway)}) + for pos := c.First(); pos != nil; pos = c.Next() { + ip := pos.IP.String() + v, _ := a.kv.Get(ip) + vs := string(v) + if v == "" { + err = a.kv.Set(ip, []byte(name)) + if err != nil { + return + } + err = a.kv.Set(name, []byte(ip)) + if err != nil { + a.kv.Delete(ip) + return + } + var gw *net.IPNet + *gw = *a.gateway + gw.IP = pos.IP + addr = gw.String() + return + } + } + } + err = fmt.Errorf("no available IPs in pool") + return +} + +func (a *Alloc) FreeIP(ctx context.Context, name string) (err error) { + if name == "" { + return + } + v, err := a.kv.Get(name) + if err != nil { + return + } + vs := string(v) + err = a.kv.Delete(vs) + if err != nil { + return + } + err = a.kv.Delete(name) + if err != nil { + a.kv.Set(vs, []byte(name)) + } + return +} + +func parseRange(cidrs []string) (nets []*net.IPNet, err error) { + for _, cidr := range cidrs { + if !strings.Contains(cidr, "-") { + _, n, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + nets = append(nets, n) + continue + } + + fs := strings.SplitN(cidr, "-", 2) + if len(fs) != 2 { + return nil, fmt.Errorf("invalid IP range %q", cidr) + } + start := net.ParseIP(strings.TrimSpace(fs[0])) + if start == nil { + return nil, fmt.Errorf("invalid IP range %q: invalid start IP %q", cidr, fs[0]) + } + end := net.ParseIP(strings.TrimSpace(fs[1])) + if end == nil { + return nil, fmt.Errorf("invalid IP range %q: invalid end IP %q", cidr, fs[1]) + } + + for _, pfx := range ipaddr.Summarize(start, end) { + n := &net.IPNet{ + IP: pfx.IP, + Mask: pfx.Mask, + } + nets = append(nets, n) + } + } + return +} diff --git a/pkg/api/v1/server/auth.go b/pkg/api/v1/server/auth.go new file mode 100644 index 0000000..404eea9 --- /dev/null +++ b/pkg/api/v1/server/auth.go @@ -0,0 +1,113 @@ +package server + +import ( + "git.gitfish.de/ston1th/haproxy-lb/pkg/config" + "golang.org/x/crypto/bcrypt" + weakrand "math/rand" + "net/http" + "sync" + "time" +) + +var cacheSize = 100 + +func init() { + weakrand.Seed(time.Now().UnixNano()) +} + +type cache struct { + cache map[string]bool + mtx sync.Mutex +} + +func newCache() *cache { + return &cache{ + cache: make(map[string]bool), + } +} + +func (c *cache) get(key string) (bool, bool) { + c.mtx.Lock() + defer c.mtx.Unlock() + v, ok := c.cache[key] + return v, ok +} + +func (c *cache) set(key string, value bool) { + c.mtx.Lock() + defer c.mtx.Unlock() + c.makeRoom() + c.cache[key] = value +} + +func (c *cache) makeRoom() { + if len(c.cache) < cacheSize { + return + } + numToDelete := len(c.cache) / 10 + if numToDelete < 1 { + numToDelete = 1 + } + for deleted := 0; deleted <= numToDelete; deleted++ { + rnd := weakrand.Intn(len(c.cache)) + i := 0 + for key := range c.cache { + if i == rnd { + delete(c.cache, key) + break + } + i++ + } + } +} + +type auth struct { + users map[string]*creds + cache *cache + // bcryptMtx is there to ensure that bcrypt.CompareHashAndPassword is run + // only once in parallel as this is CPU intensive. + bcryptMtx sync.Mutex +} + +func (a *auth) Auth(user, pass, path string) bool { + creds, valid := a.users[user] + hash := creds.Hash + + if !valid { + // The user is not found. Use a fixed password hash to + // prevent user enumeration by timing requests. + // This is a bcrypt-hashed version of "fakepassword". + hash = "$2y$10$QOauhQNbBCuQDKes6eFzPeMqBSjb7Mr5DUmpZ/VcEd00UAV/LDeSi" + } + + cacheKey := hex.EncodeToString(append(append([]byte(user), []byte(hash)...), []byte(pass)...)) + authOk, ok := a.cache.get(cacheKey) + + if !ok { + // This user, hashedPassword, password is not cached. + a.bcryptMtx.Lock() + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass)) + a.bcryptMtx.Unlock() + + authOk = err == nil + u.cache.set(cacheKey, authOk) + } + + return authOk && valid && strings.HasPrefix(path, creds.Prefix) +} + +type creds struct { + Hash string + Prefix string +} + +func newAuth(conf config.Config) (a *auth) { + a = &auth{ + users: make(map[string]creds), + cache: newCache(), + } + for _, u := range conf.BasicAuth { + a.users[u.User] = &creds{u.Hash, u.Prefix} + } + return +} diff --git a/pkg/api/v1/server/handler.go b/pkg/api/v1/server/handler.go index 5cd41c7..9e55e85 100644 --- a/pkg/api/v1/server/handler.go +++ b/pkg/api/v1/server/handler.go @@ -1,6 +1,7 @@ package server import ( + //"golang.org/x/crypto/bcrypt" "net/http" ) @@ -15,10 +16,14 @@ func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func authHandler(h ctxHandler) ctxHandler { return func(ctx *Context) { // TODO + // test:123456 + // test:$2b$10$zNfGTAQ94vifj2wtGsv1W.yZRe4/rDBzQixpB6FbrTed4BnHuNqBS + //bcrypt.CompareHashAndPassword(hash, pw) h(ctx) } } func healthzHandler(ctx *Context) { + // TODO maybe report etcd/raft stats ctx.OK() } diff --git a/pkg/api/v1/server/routes.go b/pkg/api/v1/server/routes.go index 9027ae4..8b76d91 100644 --- a/pkg/api/v1/server/routes.go +++ b/pkg/api/v1/server/routes.go @@ -2,10 +2,10 @@ package server import ( "git.giftfish.de/ston1th/haproxy-lb/pkg/api" - "git.giftfish.de/ston1th/haproxy-lb/pkg/api/v1/schema" + schemav1 "git.giftfish.de/ston1th/haproxy-lb/pkg/api/v1/schema" ) -const version = "/" + schema.Version +const v1 = "/" + schemav1.Version var routes = []api.Route{ { @@ -14,8 +14,13 @@ var routes = []api.Route{ []string{"GET"}, }, { - version + "/alloc", - authHandler(allocHandler), - []string{"POST"}, + v1 + "/lb", + authHandler(lbListHandler), + []string{"GET"}, + }, + { + v1 + "/lb/{cluster:[a-zA-Z0-9-]+}/{name:[a-zA-Z0-9-]+$}", + authHandler(lbHandler), + []string{"GET", "POST", "DELETE"}, }, } diff --git a/pkg/api/v1/server/server.go b/pkg/api/v1/server/server.go index dcf3ef4..b7770a7 100644 --- a/pkg/api/v1/server/server.go +++ b/pkg/api/v1/server/server.go @@ -34,7 +34,7 @@ type Server struct { func NewServer(log logr.Logger) *Server { s := &Server{ mux: mux.NewRouter(), - log: log, + log: log.WithName("api"), stop: make(chan struct{}), stopKeyReset: make(chan struct{}), diff --git a/pkg/config/config.go b/pkg/config/config.go index 6827588..e86965d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "golang.org/x/crypto/bcrypt" "gopkg.in/yaml.v2" ) @@ -109,16 +110,35 @@ func Validate(c *Config) error { if raft != nil && etcd != nil { return errors.New("only one cluster config allowed: raft or etcd") } + if len(c.BasicAuth) == 0 { + return errors.New("missing basic auth configuration for api users") + } + for _, u := range c.BasicAuth { + if u.User == "" { + return errors.New("basic auth username can not be empty") + } + _, err := bcrypt.Cost([]byte(u.Hash)) + if u.Hash == "" { + return fmt.Errorf("invalid basic auth hash for user %s: %w", u.User, err) + } + } return nil } type Config struct { - VirtualIPs []string `yaml:"virtualIPs"` - Interface string `yaml:"interface,omitempty"` - Label string `yaml:"label,omitempty"` - LeaderHook string `yaml:"leaderHook,omitempty"` - FollowerHook string `yaml:"followerHook,omitempty"` - Cluster Cluster `yaml:"cluster"` + VirtualIPs []string `yaml:"virtualIPs"` + Interface string `yaml:"interface,omitempty"` + Label string `yaml:"label,omitempty"` + LeaderHook string `yaml:"leaderHook,omitempty"` + FollowerHook string `yaml:"followerHook,omitempty"` + Cluster Cluster `yaml:"cluster"` + BasicAuth []BasicAuth `yaml:"basicAuth"` +} + +type BasicAuth struct { + User string `yaml:"user"` + Hash string `yaml:"hash"` + Prefix string `yaml:"prefix"` } type Cluster struct {