14 Commits

Author SHA1 Message Date
6a0d8ca436 [sandbox] Print error message on seccomp sandbox violation
Co-authored-by: cpunch <sethtstubbs@gmail.com>
2024-10-13 20:09:22 +02:00
0e32a8974f Add make target for building without Landlock 2024-10-13 03:30:53 +02:00
c196171034 [sandbox] Add backwards compatibility support for Landlock
* Support disabling Landlock at compile time or runtime if unsupported,
  without disabling seccomp
* Support older Landlock ABI versions
* Support an extra arbitrary RW path, inteded for the core dump dir
* Support database locations other than the working directory
2024-10-13 03:30:53 +02:00
8137921154 [sandbox] Initial Landlock support 2024-10-12 17:49:16 +02:00
7c66041a6f Add make target for building without the sandbox 2024-10-12 15:42:26 +02:00
2c822e210b [bcrypt] Fix missing include on Windows
Co-authored-by: Jade Shrinemaiden <jadeshrinemaiden@gmail.com>
2024-10-12 15:15:36 +02:00
CakeLancelot
c116794c83 README updates
Create dockerhub pull badge
Change links from wiki to website
2024-10-08 22:00:45 -05:00
CakeLancelot
4ebda6066c Docker: add manual workflow dispatch and auto install QEMU 2024-10-08 21:52:05 -05:00
6de21277d6 Fix eruption attacks cancelling when no targets are in range 2024-10-08 19:18:00 -07:00
CakeLancelot
397700e909 Update version numbers in README 2024-10-06 23:26:36 -05:00
d9b6aedd5b Fix nested ifdefs for windows 2024-10-06 20:58:20 -07:00
CakeLancelot
145113062b Update tdata reference to fix invalid label 2024-10-05 16:51:39 -05:00
d717c5d74d Update tdata reference for the car paths 2024-09-22 04:28:28 +02:00
a6eb0e2349 Auth Cookie Support (#285)
* Auth cookie support

* Add config option for auth cookie support

* Safe handling of TEGid/auth_id strings

* Fix bad size calculation due to pointer cast

* Expiration timestamp instead of valid bit

* Change setting to "allowed auth methods"

This allows plaintext password auth to be disabled altogether

* PR feedback
2024-09-17 20:41:48 -07:00
20 changed files with 415 additions and 56 deletions

View File

@@ -3,6 +3,7 @@ name: Push Docker Image
on:
release:
types: [published]
workflow_dispatch:
jobs:
push-docker-image:
@@ -28,6 +29,8 @@ jobs:
with:
password: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Build and push the Docker image
uses: docker/build-push-action@v5
with:

View File

@@ -133,6 +133,8 @@ HDR=$(CHDR) $(CXXHDR)
all: $(SERVER)
windows: $(SERVER)
nosandbox: $(SERVER)
nolandlock: $(SERVER)
# assign Windows-specific values if targeting Windows
windows : CC=$(WIN_CC)
@@ -142,6 +144,9 @@ windows : CXXFLAGS=$(WIN_CXXFLAGS)
windows : LDFLAGS=$(WIN_LDFLAGS)
windows : SERVER=$(WIN_SERVER)
nosandbox : CFLAGS+=-DCONFIG_NOSANDBOX=1
nolandlock : CFLAGS+=-DCONFIG_NOLANDLOCK=1
.SUFFIXES: .o .c .cpp .h .hpp
.c.o:
@@ -163,7 +168,7 @@ version.h:
src/main.o: version.h
.PHONY: all windows clean nuke
.PHONY: all windows nosandbox nolandlock clean nuke
# only gets rid of OpenFusion objects, so we don't need to
# recompile the libs every time

View File

@@ -1,34 +1,35 @@
<p align="center"><img width="640" src="res/openfusion-hero.png" alt=""></p>
<p align="center"><img width="640" src="res/openfusion-hero.png" alt="OpenFusion Logo"></p>
<p align="center">
<a href="https://github.com/OpenFusionProject/OpenFusion/releases/latest"><img src="https://img.shields.io/github/v/release/OpenFusionProject/OpenFusion" alt="Current Release"></a>
<a href="https://github.com/OpenFusionProject/OpenFusion/actions/workflows/check-builds.yaml"><img src="https://github.com/OpenFusionProject/OpenFusion/actions/workflows/check-builds.yaml/badge.svg" alt="Workflow"></a>
<a href="https://hub.docker.com/repository/docker/openfusion/openfusion/"><img src="https://badgen.net/docker/pulls/openfusion/openfusion?icon=docker&label=pulls"></a>
<a href="https://discord.gg/DYavckB"><img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg?logo=discord" alt="Discord"></a>
<a href="https://github.com/OpenFusionProject/OpenFusion/blob/master/LICENSE.md"><img src="https://img.shields.io/github/license/OpenFusionProject/OpenFusion" alt="License"></a>
</p>
OpenFusion is a reverse-engineered server for FusionFall. It primarily targets versions `beta-20100104` and `beta-20111013` of the original game, with [limited support](https://github.com/OpenFusionProject/OpenFusion/wiki/FusionFall-Version-Support) for others.
OpenFusion is a reverse-engineered server for FusionFall. It primarily targets versions `beta-20100104` and `beta-20111013` of the original game, with [limited support](https://openfusion.dev/docs/reference/fusionfall-version-support/) for others.
## Usage
### Getting Started
#### Method A: Installer (Easiest)
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5.2/OpenFusionClient-1.5.2-Installer.exe) - choose to run the file.
1. Download the client installer by clicking [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.6/OpenFusionClient-1.6-Installer.exe) - choose to run the file.
2. After a few moments, the client should open: you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
3. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
4. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 3.
#### Method B: Standalone .zip file
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.5.2/OpenFusionClient-1.5.2.zip).
1. Download the client from [here](https://github.com/OpenFusionProject/OpenFusion/releases/download/1.6/OpenFusionClient-1.6.zip).
2. Extract it to a folder of your choice. Note: if you are upgrading from an older version, it is preferable to start with a fresh folder rather than overwriting a previous install.
3. Run OpenFusionClient.exe - you will be given a choice between two public servers by default. Select the one you wish to play and click connect.
4. To create an account, simply enter the details you wish to use at the login screen then click Log In. Do *not* click register, as this will just lead to a blank screen.
5. Make a new character, and enjoy the game! Your progress will be saved automatically, and you can resume playing by entering the login details you used in step 4.
Instructions for getting the client to run on Linux through Wine can be found [here](https://github.com/OpenFusionProject/OpenFusion/wiki/Running-the-game-client-on-Linux).
Instructions for getting the client to run on Linux through Wine can be found [here](https://openfusion.dev/docs/guides/running-on-linux/).
### Hosting a server
1. Grab `OpenFusionServer-1.5-Original.zip` or `OpenFusionServer-1.5-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.5.2).
1. Grab `OpenFusionServer-1.6-Original.zip` or `OpenFusionServer-1.6-Academy.zip` from [here](https://github.com/OpenFusionProject/OpenFusion/releases/tag/1.6).
2. Extract it to a folder of your choice, then run `winfusion.exe` (Windows) or `fusion` (Linux) to start the server.
3. Add a new server to the client's list:
1. For Description, enter anything you want. This is what will show up in the server list.
@@ -79,17 +80,17 @@ This just works if you're all under the same LAN, but if you want to play over t
## Compiling
OpenFusion has one external dependency: SQLite. The oldest compatible version is `3.33.0`. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Installing-SQLite-on-Windows-using-vcpkg).
OpenFusion has one external dependency: SQLite. The oldest compatible version is `3.33.0`. You can install it on Windows using `vcpkg`, and on Unix/Linux using your distribution's package manager. For a more indepth guide on how to set up vcpkg, [read this guide](https://openfusion.dev/docs/development/installing-sqlite-on-windows-using-vcpkg/).
You have two choices for compiling OpenFusion: the included Makefile and the included CMakeLists file.
### Makefile
A detailed compilation guide is available for Windows users in the wiki [using MinGW-w64 and MSYS2](https://github.com/OpenFusionProject/OpenFusion/wiki/Compilation-on-Windows). Otherwise, to compile it for the current platform you're on, just run `make` with the correct build tools installed (currently make and clang).
A detailed compilation guide is available for Windows users on the website [using MinGW-w64 and MSYS2](https://openfusion.dev/docs/development/compilation-on-windows-msys2-mingw/). Otherwise, to compile it for the current platform you're on, just run `make` with the correct build tools installed (currently make and clang).
### CMake
A detailed guide is available [on the wiki](https://github.com/OpenFusionProject/OpenFusion/wiki/Compilation-with-CMake-or-Visual-Studio) for people using regular old CMake or the version of CMake that comes with Visual Studio. tl;dr: `cmake -B build`
A detailed guide is available [in our documentation](https://openfusion.dev/docs/development/compilation-with-cmake-or-visual-studio/) for people using regular old CMake or the version of CMake that comes with Visual Studio. TL;DR: `cmake -B build`
## Contributing
@@ -107,4 +108,4 @@ Meanwhile the Academy server is more meant for legitimate playthroughs (default
When hosting a local server, you will have access to all commands by default (account level 1).
For a list of available commands, see [this wiki page](https://github.com/OpenFusionProject/OpenFusion/wiki/Ingame-Command-list).
For a list of available commands, see [this page](https://openfusion.dev/docs/reference/ingame-command-list/).

View File

@@ -17,6 +17,10 @@ acceptallcustomnames=true
# should attempts to log into non-existent accounts
# automatically create them?
autocreateaccounts=true
# list of supported authentication methods (comma-separated)
# password = allow login type 1 with plaintext passwords
# cookie = allow login type 2 with one-shot auth cookies
authmethods=password
# how often should everything be flushed to the database?
# the default is 4 minutes
dbsaveinterval=240

19
sql/migration4.sql Normal file
View File

@@ -0,0 +1,19 @@
/*
It is recommended in the SQLite manual to turn off
foreign keys when making schema changes that involve them
*/
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
-- New table to store auth cookies
CREATE TABLE Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Expires INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);
-- Update DB Version
UPDATE Meta SET Value = 5 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@@ -143,7 +143,7 @@ CREATE TABLE IF NOT EXISTS EmailItems (
UNIQUE (PlayerID, MsgIndex, Slot)
);
CREATE TABLE IF NOT EXISTS RaceResults(
CREATE TABLE IF NOT EXISTS RaceResults (
EPID INTEGER NOT NULL,
PlayerID INTEGER NOT NULL,
Score INTEGER NOT NULL,
@@ -153,9 +153,17 @@ CREATE TABLE IF NOT EXISTS RaceResults(
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS RedeemedCodes(
CREATE TABLE IF NOT EXISTS RedeemedCodes (
PlayerID INTEGER NOT NULL,
Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code)
)
);
CREATE TABLE IF NOT EXISTS Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Expires INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);

View File

@@ -368,7 +368,6 @@ void Abilities::useNPCSkill(EntityRef npc, int skillID, std::vector<ICombatant*>
SkillData* skill = &SkillTable[skillID];
std::vector<SkillResult> results = handleSkill(skill, 0, src, affected);
if(results.empty()) return; // no effect; no need for confirmation packets
// lazy validation since skill results might be different sizes
if (!validOutVarPacket(sizeof(sP_FE2CL_NPC_SKILL_HIT), results.size(), MAX_SKILLRESULT_SIZE)) {

View File

@@ -40,6 +40,7 @@
// wrapper for U16toU8
#define ARRLEN(x) (sizeof(x)/sizeof(*x))
#define AUTOU8(x) std::string((char*)x, ARRLEN(x))
#define AUTOU16TOU8(x) U16toU8(x, ARRLEN(x))
// TODO: rewrite U16toU8 & U8toU16 to not use codecvt

View File

@@ -5,7 +5,7 @@
#include <string>
#include <vector>
#define DATABASE_VERSION 4
#define DATABASE_VERSION 5
namespace Database {
@@ -53,6 +53,10 @@ namespace Database {
void updateAccountLevel(int accountId, int accountLevel);
// return true if cookie is valid for the account.
// invalidates the stored cookie afterwards
bool checkCookie(int accountId, const char *cookie);
// interface for the /ban command
bool banPlayer(int playerId, std::string& reason);
bool unbanPlayer(int playerId);

View File

@@ -98,6 +98,55 @@ void Database::updateAccountLevel(int accountId, int accountLevel) {
sqlite3_finalize(stmt);
}
bool Database::checkCookie(int accountId, const char *tryCookie) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql_get = R"(
SELECT Cookie
FROM Auth
WHERE AccountID = ? AND Expires > ?;
)";
const char* sql_invalidate = R"(
UPDATE Auth
SET Expires = 0
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql_get, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
sqlite3_bind_int(stmt, 2, getTimestamp());
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return false;
}
const char *cookie = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (strlen(cookie) != strlen(tryCookie)) {
sqlite3_finalize(stmt);
return false;
}
/*
* since cookies are immediately invalidated, we don't need to be concerned about
* timing-related side channel attacks, so strcmp is fine here
*/
bool match = (strcmp(cookie, tryCookie) == 0);
sqlite3_finalize(stmt);
sqlite3_prepare_v2(db, sql_invalidate, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on checkCookie(): " << sqlite3_errmsg(db) << std::endl;
return match;
}
void Database::updateSelected(int accountId, int slot) {
std::lock_guard<std::mutex> lock(dbCrit);

View File

@@ -49,6 +49,7 @@ CNShardServer *shardServer = nullptr;
std::thread *shardThread = nullptr;
void startShard(CNShardServer* server) {
sandbox_thread_start();
server->start();
}
@@ -150,6 +151,8 @@ int main() {
/* not reached */
}
sandbox_init();
std::cout << "[INFO] Starting Server Threads..." << std::endl;
CNLoginServer loginServer(settings::LOGINPORT);
shardServer = new CNShardServer(settings::SHARDPORT);
@@ -157,6 +160,7 @@ int main() {
shardThread = new std::thread(startShard, (CNShardServer*)shardServer);
sandbox_start();
sandbox_thread_start();
loginServer.start();

View File

@@ -4,11 +4,16 @@
#if defined(__linux__) || defined(__OpenBSD__)
# if !defined(CONFIG_NOSANDBOX)
void sandbox_init();
void sandbox_start();
void sandbox_thread_start();
# else
#include <iostream>
inline void sandbox_init() {}
inline void sandbox_thread_start() {}
inline void sandbox_start() {
std::cout << "[WARN] Built without a sandbox" << std::endl;
}
@@ -17,5 +22,7 @@ inline void sandbox_start() {
#else
// stub for unsupported platforms
inline void sandbox_init() {}
inline void sandbox_start() {}
inline void sandbox_thread_start() {}
#endif

View File

@@ -13,6 +13,9 @@ static void eunveil(const char *path, const char *permissions) {
err(1, "unveil");
}
void sandbox_init() {}
void sandbox_thread_start() {}
void sandbox_start() {
/*
* There shouldn't ever be a reason to disable this one, but might as well

View File

@@ -4,6 +4,9 @@
#include "settings.hpp"
#include <stdlib.h>
#include <fcntl.h>
#include <filesystem>
#include <sys/prctl.h>
#include <sys/ptrace.h>
@@ -17,6 +20,10 @@
#include <linux/audit.h>
#include <linux/net.h> // for socketcall() args
#ifndef CONFIG_NOLANDLOCK
#include <linux/landlock.h>
#endif
/*
* Macros adapted from https://outflux.net/teach-seccomp/
* Relevant license:
@@ -54,7 +61,7 @@
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ERRNO|(_errno))
#define KILL_PROCESS \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL_PROCESS)
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_TRAP)
/*
* Macros adapted from openssh's sandbox-seccomp-filter.c
@@ -297,25 +304,201 @@ static sock_fprog prog = {
ARRLEN(filter), filter
};
// our own wrapper for the seccomp() syscall
// Our own wrapper for the seccomp() syscall.
int seccomp(unsigned int operation, unsigned int flags, void *args) {
return syscall(__NR_seccomp, operation, flags, args);
}
void sandbox_start() {
#ifndef CONFIG_NOLANDLOCK
// Support compilation on systems that only have older Landlock headers.
#ifndef LANDLOCK_ACCESS_FS_REFER
#define LANDLOCK_ACCESS_FS_REFER 0
#endif
#ifndef LANDLOCK_ACCESS_FS_TRUNCATE
#define LANDLOCK_ACCESS_FS_TRUNCATE 0
#endif
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE
| LANDLOCK_ACCESS_FS_WRITE_FILE
| LANDLOCK_ACCESS_FS_READ_DIR
| LANDLOCK_ACCESS_FS_MAKE_REG
| LANDLOCK_ACCESS_FS_MAKE_DIR
| LANDLOCK_ACCESS_FS_MAKE_SYM
| LANDLOCK_ACCESS_FS_MAKE_SOCK
| LANDLOCK_ACCESS_FS_MAKE_FIFO
| LANDLOCK_ACCESS_FS_MAKE_BLOCK
| LANDLOCK_ACCESS_FS_REMOVE_FILE
| LANDLOCK_ACCESS_FS_REMOVE_DIR
| LANDLOCK_ACCESS_FS_TRUNCATE
| LANDLOCK_ACCESS_FS_REFER
};
uint64_t landlock_perms = LANDLOCK_ACCESS_FS_READ_FILE
| LANDLOCK_ACCESS_FS_WRITE_FILE
| LANDLOCK_ACCESS_FS_TRUNCATE
| LANDLOCK_ACCESS_FS_MAKE_REG
| LANDLOCK_ACCESS_FS_REMOVE_FILE;
int landlock_fd;
bool landlock_supported;
/*
* Our own wrappers for Landlock syscalls.
*/
int landlock_create_ruleset(const struct landlock_ruleset_attr *attr, size_t size, uint32_t flags) {
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}
int landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type, const void *rule_attr, uint32_t flags) {
return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
}
int landlock_restrict_self(int ruleset_fd, uint32_t flags) {
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}
static void landlock_path(std::string path, uint32_t perms) {
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = perms
};
path_beneath.parent_fd = open(path.c_str(), O_PATH|O_CLOEXEC);
if (path_beneath.parent_fd < 0) {
perror(path.c_str());
exit(1);
}
if (landlock_add_rule(landlock_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) {
perror("landlock_add_rule");
exit(1);
}
close(path_beneath.parent_fd);
}
static bool landlock_detect() {
int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 0) {
if (errno == ENOSYS || errno == EOPNOTSUPP) {
std::cout << "[WARN] No Landlock support on this system" << std::endl;
return false;
}
perror("landlock_create_ruleset");
exit(1);
}
std::cout << "[INFO] Landlock ABI version: " << abi << std::endl;
switch (abi) {
case 1:
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
landlock_perms &= ~LANDLOCK_ACCESS_FS_REFER;
// fallthrough
case 2:
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
landlock_perms &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
}
return true;
}
static void landlock_init() {
std::cout << "[INFO] Setting up Landlock sandbox..." << std::endl;
landlock_supported = landlock_detect();
if (!landlock_supported)
return;
landlock_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (landlock_fd < 0) {
perror("landlock_create_ruleset");
exit(1);
}
std::string dbdir = std::filesystem::path(settings::DBPATH).parent_path();
// for the DB files (we can't rely on them being in the working directory)
landlock_path(dbdir == "" ? "." : dbdir, landlock_perms);
// for writing the gruntwork file
landlock_path(settings::TDATADIR, landlock_perms);
// for passowrd salting during account creation
landlock_path("/dev/urandom", LANDLOCK_ACCESS_FS_READ_FILE);
// for core dumps, optionally
if (settings::SANDBOXEXTRAPATH != "")
landlock_path(settings::SANDBOXEXTRAPATH, landlock_perms);
}
#endif // !CONFIG_NOLANDLOCK
static void sigsys_handler(int signo, siginfo_t *info, void *context) {
// report the unhandled syscall
std::cout << "[FATAL] Unhandled syscall " << info->si_syscall
<< " at " << std::hex << info->si_call_addr << " on arch " << info->si_arch << std::endl;
std::cout << "If you're unsure why this is happening, please read https://openfusion.dev/docs/development/the-sandbox/" << std::endl
<< "for more information and possibly open an issue at https://github.com/OpenFusionProject/OpenFusion/issues to report"
<< " needed changes in our seccomp filter." << std::endl;
exit(1);
}
void sandbox_init() {
if (!settings::SANDBOX) {
std::cout << "[WARN] Running without a sandbox" << std::endl;
return;
}
// listen to SIGSYS to report unhandled syscalls
struct sigaction sa = {};
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = sigsys_handler;
if (sigaction(SIGSYS, &sa, NULL) < 0) {
perror("sigaction");
exit(1);
}
#ifndef CONFIG_NOLANDLOCK
landlock_init();
#else
std::cout << "[WARN] Built without Landlock" << std::endl;
#endif
}
void sandbox_start() {
if (!settings::SANDBOX)
return;
std::cout << "[INFO] Starting seccomp-bpf sandbox..." << std::endl;
// Sandboxing starts in sandbox_thread_start().
}
void sandbox_thread_start() {
if (!settings::SANDBOX)
return;
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) {
perror("prctl");
exit(1);
}
if (seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, &prog) < 0) {
#ifndef CONFIG_NOLANDLOCK
if (landlock_supported) {
if (landlock_restrict_self(landlock_fd, 0)) {
perror("landlock_restrict_self");
exit(1);
}
}
#endif
if (seccomp(SECCOMP_SET_MODE_FILTER, 0, &prog) < 0) {
perror("seccomp");
exit(1);
}

View File

@@ -105,57 +105,95 @@ void loginFail(LoginError errorCode, std::string userLogin, CNSocket* sock) {
void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
auto login = (sP_CL2LS_REQ_LOGIN*)data->buf;
// TODO: implement better way of sending credentials
std::string userLogin((char*)login->szCookie_TEGid);
std::string userPassword((char*)login->szCookie_authid);
std::string userLogin;
std::string userToken; // could be password or auth cookie
/*
* Sometimes the client sends garbage cookie data.
* Validate it as normal credentials instead of using a length check before falling back.
* The std::string -> char* -> std::string maneuver should remove any
* trailing garbage after the null terminator.
*/
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
/*
* The std::string -> char* -> std::string maneuver should remove any
* trailing garbage after the null terminator.
*/
if (login->iLoginType == (int32_t)LoginType::COOKIE) {
userLogin = std::string(AUTOU8(login->szCookie_TEGid).c_str());
userToken = std::string(AUTOU8(login->szCookie_authid).c_str());
} else {
userLogin = std::string(AUTOU16TOU8(login->szID).c_str());
userPassword = std::string(AUTOU16TOU8(login->szPassword).c_str());
userToken = std::string(AUTOU16TOU8(login->szPassword).c_str());
}
// the client inserts a "\n" in the password if you press enter key in the middle of the password
// (not at the start or the end of the password field)
if (int(userPassword.find("\n")) > 0)
userPassword.erase(userPassword.find("\n"), 1);
// check regex
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
// check username regex
if (!CNLoginServer::isUsernameGood(userLogin)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid login or password\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore\n";
text += "Password has to be 8 - 32 characters long";
std::string text = "Invalid login\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 15;
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
// we only interpret the token as a cookie if cookie login was used and it's allowed.
// otherwise we interpret it as a password, and this maintains compatibility with
// the auto-login trick used on older clients
bool isCookieAuth = login->iLoginType == (int32_t)LoginType::COOKIE
&& CNLoginServer::isLoginTypeAllowed(LoginType::COOKIE);
// password login checks
if (!isCookieAuth) {
// bail if password auth isn't allowed
if (!CNLoginServer::isLoginTypeAllowed(LoginType::PASSWORD)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Password login disabled\n";
text += "This server has disabled logging in with plaintext passwords.\n";
text += "Please contact an admin for assistance.";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 12;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
// check regex
if (!CNLoginServer::isPasswordGood(userToken)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid password\n";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
}
Database::Account findUser = {};
Database::findAccount(&findUser, userLogin);
// account was not found
if (findUser.AccountID == 0) {
if (settings::AUTOCREATEACCOUNTS)
return newAccount(sock, userLogin, userPassword, login->iClientVerC);
// don't auto-create an account if it's a cookie auth for whatever reason
if (settings::AUTOCREATEACCOUNTS && !isCookieAuth)
return newAccount(sock, userLogin, userToken, login->iClientVerC);
return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock);
}
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
if (isCookieAuth) {
const char *cookie = userToken.c_str();
if (!Database::checkCookie(findUser.AccountID, cookie))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
} else {
// simple password check
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userToken))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
}
// is the account banned
if (findUser.BannedUntil > getTimestamp()) {
@@ -621,11 +659,14 @@ bool CNLoginServer::exitDuplicate(int accountId) {
return false;
}
bool CNLoginServer::isLoginDataGood(std::string login, std::string password) {
std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
bool CNLoginServer::isUsernameGood(std::string login) {
const std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
return (std::regex_match(login, loginRegex));
}
return (std::regex_match(login, loginRegex) && std::regex_match(password, passwordRegex));
bool CNLoginServer::isPasswordGood(std::string password) {
const std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
return (std::regex_match(password, passwordRegex));
}
bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword) {
@@ -638,4 +679,17 @@ bool CNLoginServer::isCharacterNameGood(std::string Firstname, std::string Lastn
std::regex lastnamecheck(R"(((?! )(?!\.)[a-zA-Z0-9]*\.{0,1}(?!\.+ +)[a-zA-Z0-9]* {0,1}(?! +))*$)");
return (std::regex_match(Firstname, firstnamecheck) && std::regex_match(Lastname, lastnamecheck));
}
bool CNLoginServer::isLoginTypeAllowed(LoginType loginType) {
// the config file specifies "comma-separated" but tbh we don't care
switch (loginType) {
case LoginType::PASSWORD:
return settings::AUTHMETHODS.find("password") != std::string::npos;
case LoginType::COOKIE:
return settings::AUTHMETHODS.find("cookie") != std::string::npos;
default:
break;
}
return false;
}
#pragma endregion

View File

@@ -23,6 +23,11 @@ enum class LoginError {
UPDATED_EUALA_REQUIRED = 9
};
enum class LoginType {
PASSWORD = 1,
COOKIE = 2
};
// WARNING: THERE CAN ONLY BE ONE OF THESE SERVERS AT A TIME!!!!!! TODO: change loginSessions & packet handlers to be non-static
class CNLoginServer : public CNServer {
private:
@@ -39,10 +44,12 @@ private:
static void changeName(CNSocket* sock, CNPacketData* data);
static void duplicateExit(CNSocket* sock, CNPacketData* data);
static bool isLoginDataGood(std::string login, std::string password);
static bool isUsernameGood(std::string login);
static bool isPasswordGood(std::string password);
static bool isPasswordCorrect(std::string actualPassword, std::string tryPassword);
static bool isAccountInUse(int accountId);
static bool isCharacterNameGood(std::string Firstname, std::string Lastname);
static bool isLoginTypeAllowed(LoginType loginType);
static void newAccount(CNSocket* sock, std::string userLogin, std::string userPassword, int32_t clientVerC);
// returns true if success
static bool exitDuplicate(int accountId);

View File

@@ -9,10 +9,12 @@
// defaults :)
int settings::VERBOSITY = 1;
bool settings::SANDBOX = true;
std::string settings::SANDBOXEXTRAPATH = "";
int settings::LOGINPORT = 23000;
bool settings::APPROVEALLNAMES = true;
bool settings::AUTOCREATEACCOUNTS = true;
std::string settings::AUTHMETHODS = "password";
int settings::DBSAVEINTERVAL = 240;
int settings::SHARDPORT = 23001;
@@ -84,9 +86,11 @@ void settings::init() {
VERBOSITY = reader.GetInteger("", "verbosity", VERBOSITY);
SANDBOX = reader.GetBoolean("", "sandbox", SANDBOX);
SANDBOXEXTRAPATH = reader.Get("", "sandboxextrapath", SANDBOXEXTRAPATH);
LOGINPORT = reader.GetInteger("login", "port", LOGINPORT);
APPROVEALLNAMES = reader.GetBoolean("login", "acceptallcustomnames", APPROVEALLNAMES);
AUTOCREATEACCOUNTS = reader.GetBoolean("login", "autocreateaccounts", AUTOCREATEACCOUNTS);
AUTHMETHODS = reader.Get("login", "authmethods", AUTHMETHODS);
DBSAVEINTERVAL = reader.GetInteger("login", "dbsaveinterval", DBSAVEINTERVAL);
SHARDPORT = reader.GetInteger("shard", "port", SHARDPORT);
SHARDSERVERIP = reader.Get("shard", "ip", SHARDSERVERIP);

View File

@@ -5,9 +5,11 @@
namespace settings {
extern int VERBOSITY;
extern bool SANDBOX;
extern std::string SANDBOXEXTRAPATH;
extern int LOGINPORT;
extern bool APPROVEALLNAMES;
extern bool AUTOCREATEACCOUNTS;
extern std::string AUTHMETHODS;
extern int DBSAVEINTERVAL;
extern int SHARDPORT;
extern std::string SHARDSERVERIP;

2
tdata

Submodule tdata updated: 8c98c83682...bdb611b092

View File

@@ -22,13 +22,14 @@
#endif
#include <errno.h>
#if defined(_WIN32) || defined(_WIN64)
// On windows we need to generate random bytes differently.
#if defined(_WIN32) && !defined(_WIN64)
typedef __int32 ssize_t;
#elif defined(_WIN32) && defined(_WIN64)
typedef __int64 ssize_t;
#endif
#if defined(_WIN32) || defined(_WIN64)
// On windows we need to generate random bytes differently.
#define BCRYPT_HASHSIZE 60
#include "bcrypt.h"
@@ -37,9 +38,10 @@ typedef __int64 ssize_t;
#include <wincrypt.h> /* CryptAcquireContext, CryptGenRandom */
#else
#include "bcrypt.h"
#include "ow-crypt.h"
#endif
#include "ow-crypt.h"
#define RANDBYTES (16)
static int try_close(int fd)