aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimbeer <himbeer@disroot.org>2024-11-16 23:17:27 +0100
committerHimbeer <himbeer@disroot.org>2024-11-17 16:32:50 +0100
commitd0d39d2f9f3abb68eec146376b3233c8bc7c1cfa (patch)
tree70be515811406cdc3ba1ab8b56d3c6eccb2f0d59
parente7d92562fcf05e4f9b7f6ba29b7cda529aab49bc (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.go41
-rw-r--r--auth_mtpostgresql.go4
-rw-r--r--auth_mtsqlite3.go4
-rw-r--r--cmd/mt-auth-convert/convert.go15
-rw-r--r--doc/auth_backends.md26
-rw-r--r--hop.go2
-rw-r--r--moderation.go14
-rw-r--r--plugin.go5
-rw-r--r--plugin_auth.go74
-rw-r--r--process.go10
-rw-r--r--run.go16
11 files changed, 179 insertions, 32 deletions
diff --git a/auth.go b/auth.go
index 734eaec..45f9bdc 100644
--- a/auth.go
+++ b/auth.go
@@ -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).
diff --git a/hop.go b/hop.go
index de1774a..3179df5 100644
--- a/hop.go
+++ b/hop.go
@@ -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)
}
diff --git a/plugin.go b/plugin.go
index d0ec0a6..a31f101 100644
--- a/plugin.go
+++ b/plugin.go
@@ -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)
+ })
+}
diff --git a/process.go b/process.go
index 82b0bce..71cf0b8 100644
--- a/process.go
+++ b/process.go
@@ -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})
diff --git a/run.go b/run.go
index 703e830..edd0856 100644
--- a/run.go
+++ b/run.go
@@ -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 {