diff options
author | Himbeer <himbeer@disroot.org> | 2024-11-16 23:17:27 +0100 |
---|---|---|
committer | Himbeer <himbeer@disroot.org> | 2024-11-17 16:32:50 +0100 |
commit | d0d39d2f9f3abb68eec146376b3233c8bc7c1cfa (patch) | |
tree | 70be515811406cdc3ba1ab8b56d3c6eccb2f0d59 | |
parent | e7d92562fcf05e4f9b7f6ba29b7cda529aab49bc (diff) |
Allow plugins to implement authentication backends
Design decisions:
* Config option specifies which of the registered backends is used
* Name conflicts (including with builtins) make the backend registration
fail
* Builtin backends are not moved to plugins to avoid breaking existing
setups confusion in general
* Builtin backends are exposed to plugins (and have been for some time);
Important information and internal methods are hidden to prevent
interference from malicious plugins
See doc/auth_backends.md and the related interface and function
documentation for details.
Closes #127.
-rw-r--r-- | auth.go | 41 | ||||
-rw-r--r-- | auth_mtpostgresql.go | 4 | ||||
-rw-r--r-- | auth_mtsqlite3.go | 4 | ||||
-rw-r--r-- | cmd/mt-auth-convert/convert.go | 15 | ||||
-rw-r--r-- | doc/auth_backends.md | 26 | ||||
-rw-r--r-- | hop.go | 2 | ||||
-rw-r--r-- | moderation.go | 14 | ||||
-rw-r--r-- | plugin.go | 5 | ||||
-rw-r--r-- | plugin_auth.go | 74 | ||||
-rw-r--r-- | process.go | 10 | ||||
-rw-r--r-- | run.go | 16 |
11 files changed, 179 insertions, 32 deletions
@@ -27,23 +27,60 @@ type Ban struct { Name string } +// An AuthBackend provides authentication and moderation functionality. +// This typically includes persistent storage. +// It does not handle authorization, i.e. permission checks. +// All methods are safe for concurrent use. type AuthBackend interface { + // Exists reports whether a user with the specified name exists. + // The result is false if an error is encountered. Exists(name string) bool + // Passwd returns the SRP verifier and salt of the user + // with the specified name or an error. Passwd(name string) (salt, verifier []byte, err error) + // SetPasswd sets the SRP verifier and salt of the user + // with the specified name. SetPasswd(name string, salt, verifier []byte) error + // LastSrv returns the name of the last server the user + // with the specified name intentionally connected to or an error. + // This method should return an error if this feature is unsupported. + // Errors are handled gracefully (by connecting the user + // to the default server or group) and aren't logged. LastSrv(name string) (string, error) + // SetLastSrv sets the name of the last server the user + // with the specified name intentionally connected to. + // This method should not return an error if this feature is unsupported. + // Errors will make server hopping fail. SetLastSrv(name, srv string) error + // Timestamp returns the last time the user with the specified name + // connected to the proxy or an error. Timestamp(name string) (time.Time, error) + // Import adds or modifies authentication entries in bulk. Import(in []User) error + // Export returns all authentication entries or an error. Export() ([]User, error) + // Ban adds a ban entry for a network address and an associated name. + // Only the specified network address is banned from connecting. + // Existing connections are not kicked. Ban(addr, name string) error + // Unban deletes a ban entry by network address or username. Unban(id string) error + // Banned reports whether a network address is banned. + // The result is true if an error is encountered. Banned(addr *net.UDPAddr) bool + // ImportBans adds or modifies ban entries in bulk. ImportBans(in []Ban) error + // Export returns all ban entries or an error. ExportBans() ([]Ban, error) } +// DefaultAuth returns the authentication backend that is currently in use +// or nil during initialization time. +func DefaultAuth() AuthBackend { + return authIface +} + func setAuthBackend(ab AuthBackend) error { if authIface != nil { return ErrAuthBackendExists @@ -53,11 +90,11 @@ func setAuthBackend(ab AuthBackend) error { return nil } -func encodeVerifierAndSalt(salt, verifier []byte) string { +func EncodeVerifierAndSalt(salt, verifier []byte) string { return "#1#" + b64.EncodeToString(salt) + "#" + b64.EncodeToString(verifier) } -func decodeVerifierAndSalt(encodedPasswd string) ([]byte, []byte, error) { +func DecodeVerifierAndSalt(encodedPasswd string) ([]byte, []byte, error) { if !strings.HasPrefix(encodedPasswd, "#1#") { return nil, nil, ErrInvalidSRPHeader } diff --git a/auth_mtpostgresql.go b/auth_mtpostgresql.go index fe4b9c7..dc7f2f3 100644 --- a/auth_mtpostgresql.go +++ b/auth_mtpostgresql.go @@ -123,7 +123,7 @@ func (a *AuthMTPostgreSQL) Passwd(name string) (salt, verifier []byte, err error return } - salt, verifier, err = decodeVerifierAndSalt(encodedPasswd) + salt, verifier, err = DecodeVerifierAndSalt(encodedPasswd) a.updateTimestamp(name) return @@ -132,7 +132,7 @@ func (a *AuthMTPostgreSQL) Passwd(name string) (salt, verifier []byte, err error // SetPasswd creates a password entry if necessary // and sets the password of a user. func (a *AuthMTPostgreSQL) SetPasswd(name string, salt, verifier []byte) error { - encodedPasswd := encodeVerifierAndSalt(salt, verifier) + encodedPasswd := EncodeVerifierAndSalt(salt, verifier) _, err := a.db.Exec("INSERT INTO auth (name, password, last_login) VALUES ($1, $2, extract(epoch from now())) ON CONFLICT (name) DO UPDATE SET password = EXCLUDED.password, last_login = extract(epoch from now());", name, encodedPasswd) return err diff --git a/auth_mtsqlite3.go b/auth_mtsqlite3.go index 64bce1a..fb340c5 100644 --- a/auth_mtsqlite3.go +++ b/auth_mtsqlite3.go @@ -69,7 +69,7 @@ func (a *AuthMTSQLite3) Passwd(name string) (salt, verifier []byte, err error) { return } - salt, verifier, err = decodeVerifierAndSalt(encodedPasswd) + salt, verifier, err = DecodeVerifierAndSalt(encodedPasswd) a.updateTimestamp(name) return @@ -78,7 +78,7 @@ func (a *AuthMTSQLite3) Passwd(name string) (salt, verifier []byte, err error) { // SetPasswd creates a password entry if necessary // and sets the password of a user. func (a *AuthMTSQLite3) SetPasswd(name string, salt, verifier []byte) error { - encodedPasswd := encodeVerifierAndSalt(salt, verifier) + encodedPasswd := EncodeVerifierAndSalt(salt, verifier) _, err := a.db.Exec("REPLACE INTO auth (name, password, last_login) VALUES (?, ?, unixepoch());", name, encodedPasswd) return err diff --git a/cmd/mt-auth-convert/convert.go b/cmd/mt-auth-convert/convert.go index 28e44e9..853336f 100644 --- a/cmd/mt-auth-convert/convert.go +++ b/cmd/mt-auth-convert/convert.go @@ -9,6 +9,9 @@ where from is the format to convert from and to is the format to convert to and inconn is the postgres connection string for the source database and outconn is the postgres connection string for the destination database. + +Other backend-specific configuration is provided by the respective plugin's +configuration mechanism if needed. */ package main @@ -24,6 +27,8 @@ func main() { log.Fatal("usage: mt-auth-convert from to inconn outconn") } + proxy.LoadPlugins() + var inBackend proxy.AuthBackend switch os.Args[1] { case "files": @@ -41,7 +46,10 @@ func main() { log.Fatal(err) } default: - log.Fatal("invalid input auth backend") + var ok bool + if inBackend, ok = proxy.Auth(os.Args[1]); !ok { + log.Fatal("invalid input auth backend") + } } var outBackend proxy.AuthBackend @@ -61,7 +69,10 @@ func main() { log.Fatal(err) } default: - log.Fatal("invalid output auth backend") + var ok bool + if outBackend, ok = proxy.Auth(os.Args[2]); !ok { + log.Fatal("invalid output auth backend") + } } if err := convert(outBackend, inBackend); err != nil { diff --git a/doc/auth_backends.md b/doc/auth_backends.md index ce9dc2b..4075b08 100644 --- a/doc/auth_backends.md +++ b/doc/auth_backends.md @@ -94,3 +94,29 @@ Example (converting Minetest's PostgreSQL database to the `files` backend): ``` mt-auth-convert mtpostgresql files 'host=localhost user=mt dbname=mtauth sslmode=disable' nil ``` + +## Implementing custom authentication backends + +Plugins can implement their own authentication backends and register them +using the [RegisterAuthBackend](https://pkg.go.dev/github.com/HimbeerserverDE/mt-multiserver-proxy#RegisterAuthBackend) +function. These plugins can then be enabled by setting the `AuthBackend` +config option. Configuration for them is provided through a plugin-specific +configuration mechanism if needed. + +This makes it possible to integrate with custom environments, especially those +that share a database with other services such as a forum. + +Note that the network protocol is always SRP and the credentials are always SRP +verifiers and salts. This is a protocol limitation and should not be an issue. + +When implementing an authentication backend, make sure you follow the +[interface documentation](https://pkg.go.dev/github.com/HimbeerserverDE/mt-multiserver-proxy#AuthBackend) +carefully. The active authentication backend can be accessed using the +[DefaultAuth](https://pkg.go.dev/github.com/HimbeerserverDE/mt-multiserver-proxy#DefaultAuth) +function *after initialization time*. +Other backends can be accessed using the +[Auth](https://pkg.go.dev/github.com/HimbeerserverDE/mt-multiserver-proxy#Auth) +function *after initialization time*. + +Custom backends can be handled by the [mt-auth-convert](#mt-auth-convert) tool +as long as it is able to load the relevant plugin(s). @@ -24,7 +24,7 @@ var ( func (cc *ClientConn) Hop(serverName string) (err error) { defer func() { if err == nil && !Conf().ForceDefaultSrv { - err = authIface.SetLastSrv(cc.Name(), serverName) + err = DefaultAuth().SetLastSrv(cc.Name(), serverName) } }() diff --git a/moderation.go b/moderation.go index d10661d..00d97b1 100644 --- a/moderation.go +++ b/moderation.go @@ -27,16 +27,6 @@ func (cc *ClientConn) Kick(reason string) { // network address from connecting again. func (cc *ClientConn) Ban() error { cc.Kick("Banned by proxy.") - return authIface.Ban(cc.RemoteAddr().(*net.UDPAddr).IP.String(), cc.name) -} - -// Unban removes a player from the ban list. It accepts both -// network addresses and player names. -func Unban(id string) error { - return authIface.Unban(id) -} - -// Banned reports whether a network address is banned. -func Banned(addr *net.UDPAddr) bool { - return authIface.Banned(addr) + ip := cc.RemoteAddr().(*net.UDPAddr).IP.String() + return DefaultAuth().Ban(ip, cc.name) } @@ -64,7 +64,10 @@ func buildPluginDev(version string) error { return nil } -func loadPlugins() { +// LoadPlugins loads all plugins. Further calls are no-ops. +// This function is not intended to be called by plugins. +// It is meant for tools such as mt-auth-convert. +func LoadPlugins() { pluginsOnce.Do(openPlugins) } diff --git a/plugin_auth.go b/plugin_auth.go new file mode 100644 index 0000000..ec58189 --- /dev/null +++ b/plugin_auth.go @@ -0,0 +1,74 @@ +package proxy + +import ( + "errors" + "fmt" + "sync" +) + +var ( + ErrEmptyAuthBackendName = errors.New("auth backend name is empty") + ErrNilAuthBackend = errors.New("auth backend is nil") + ErrBuiltinAuthBackend = errors.New("auth backend name collision with builtin") +) + +var ( + authBackends map[string]AuthBackend + authBackendsMu sync.Mutex + authBackendsOnce sync.Once +) + +// Auth returns an authentication backend by name or false if it doesn't exist. +func Auth(name string) (AuthBackend, bool) { + authBackendsMu.Lock() + defer authBackendsMu.Unlock() + + ab, ok := authBackends[name] + return ab, ok +} + +// RegisterAuthBackend registers a new authentication backend implementation. +// The name must be unique, non-empty and must not collide +// with a builtin authentication backend. +// The authentication backend must be non-nil. +// Registered backends can be enabled by specifying their name +// in the AuthBackend config option. +// Backend-specific configuration is handled by the calling plugin's +// configuration mechanism at initialization time. +// Backends must be registered at initialization time +// (before the init functions return). +// Backends registered after initialization time will not be available +// to the user. +func RegisterAuthBackend(name string, ab AuthBackend) error { + initAuthBackends() + + if name == "" { + return ErrEmptyAuthBackendName + } + if ab == nil { + return ErrNilAuthBackend + } + + if name == "files" || name == "mtsqlite3" || name == "mtpostgresql" { + return ErrBuiltinAuthBackend + } + + if _, ok := authBackends[name]; ok { + return fmt.Errorf("duplicate auth backend %s", name) + } + + authBackendsMu.Lock() + defer authBackendsMu.Unlock() + + authBackends[name] = ab + return nil +} + +func initAuthBackends() { + authBackendsOnce.Do(func() { + authBackendsMu.Lock() + defer authBackendsMu.Unlock() + + authBackends = make(map[string]AuthBackend) + }) +} @@ -88,7 +88,7 @@ func (cc *ClientConn) process(pkt mt.Pkt) { cc.name = cmd.PlayerName cc.logger.SetPrefix(fmt.Sprintf("[%s %s] ", cc.RemoteAddr(), cc.Name())) - if authIface.Banned(cc.RemoteAddr().(*net.UDPAddr)) { + if DefaultAuth().Banned(cc.RemoteAddr().(*net.UDPAddr)) { cc.Log("<-", "banned") cc.Kick("Banned by proxy.") return @@ -141,7 +141,7 @@ func (cc *ClientConn) process(pkt mt.Pkt) { } // reply - if authIface.Exists(cc.Name()) { + if DefaultAuth().Exists(cc.Name()) { cc.auth.method = mt.SRP } else { cc.auth.method = mt.FirstSRP @@ -188,7 +188,7 @@ func (cc *ClientConn) process(pkt mt.Pkt) { return } - if err := authIface.SetPasswd(cc.Name(), cmd.Salt, cmd.Verifier); err != nil { + if err := DefaultAuth().SetPasswd(cc.Name(), cmd.Salt, cmd.Verifier); err != nil { cc.Log("<-", "set password fail") ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.SrvErr}) @@ -215,7 +215,7 @@ func (cc *ClientConn) process(pkt mt.Pkt) { } cc.setState(csActive) - if err := authIface.SetPasswd(cc.Name(), cmd.Salt, cmd.Verifier); err != nil { + if err := DefaultAuth().SetPasswd(cc.Name(), cmd.Salt, cmd.Verifier); err != nil { cc.Log("<-", "change password fail") cc.SendChatMsg("Password change failed or unavailable.") return @@ -258,7 +258,7 @@ func (cc *ClientConn) process(pkt mt.Pkt) { cc.auth.method = mt.SRP - salt, verifier, err := authIface.Passwd(cc.Name()) + salt, verifier, err := DefaultAuth().Passwd(cc.Name()) if err != nil { cc.Log("<-", "SRP data retrieval fail") ack, _ := cc.SendCmd(&mt.ToCltKick{Reason: mt.SrvErr}) @@ -20,11 +20,11 @@ func Run() { func runFunc() { if !Conf().NoPlugins { - loadPlugins() + LoadPlugins() } - var err error - switch Conf().AuthBackend { + authBackendName := Conf().AuthBackend + switch authBackendName { case "files": setAuthBackend(AuthFiles{}) case "mtsqlite3": @@ -41,8 +41,14 @@ func runFunc() { } setAuthBackend(ab) - default: + case "": log.Fatal("invalid auth backend") + default: + if ab, ok := authBackends[authBackendName]; ok { + setAuthBackend(ab) + } else { + log.Fatal("invalid auth backend") + } } addr, err := net.ResolveUDPAddr("udp", Conf().BindAddr) @@ -124,7 +130,7 @@ func runFunc() { } srvName, srv := conf.DefaultServerInfo() - lastSrv, err := authIface.LastSrv(cc.Name()) + lastSrv, err := DefaultAuth().LastSrv(cc.Name()) if err == nil && !conf.ForceDefaultSrv && lastSrv != srvName { choice, ok := conf.RandomGroupServer(lastSrv) if !ok { |