Guide: Implementing Custom Users Storage in Xray-core (with Redis example) #6336
thearialume
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
🤔 What is this document about?
This document covers all of the essentials for implementation of custom users storage for Xray-core and it's integration into other protocols. Everything written here doesn't imply any workarounds or hacks, it's completely compatible with all the existing Xray-core functionality.
Although, protocols based on AEAD such as
ShadowSocksandVMess, require more research due to their unique user validation logic.This document also doesn't cover protocols without
proxy.UserManagerimplementation such asHTTPandSOCKS5, they require different approach.All further information was fully tested and confirmed only for
VLESSprotocol, expect other protocols to have different structure and underlying logic.📝 A little backstory
Right now, there are no possible ways in Xray-core to dynamically authenticate users at the runtime. It only supports users management via the gRPC API. It works, but it's not great at scale, as it requires you to sync current users across every server.
So, in March of this year, I started working on external authentication for Xray Core. The main goal was to bring support for dynamic users authentication through HTTP API and CLI as it was made in Hysteria2, while also creating a simple interface for developers to create their own methods.
But exploring Xray-core, I realized that architecture of my solution was too complicated. I took one more look at the Alternative for clients storage discussion. The only reasonable solution, given the Xray-core architecture.
With the gained experience I changed project's direction from users authentication to alternative users storage. However, I never managed to finish it: it was important to me for this feature to be merged in Xray-core, but it brought so significant changes, it seemed unlikely to me. Project was shelved and eventually removed.
Last week I received a message with questions about current state of project. I told everything as it is. They told me they want to continue on this idea and in case I have anything left, it will be helpful.
Unfortunately, all the code was deleted, but I still had bunch of notes with explanations, overall architecture, methods, ideas and code snippets. So, I decided to collect them all into one document and release it to the community.
📌 Introduction to
ValidatorAll of the major protocols like
VLESS,Hysteria2,Trojan,VMessandShadowSocksare implementingValidatorinterface as their authentication backend.Although, it is not a standardized solution, all of these protocols implement it almost identically with slight difference for AEAD based protocols.
Here is exact interface found in
/proxy/vless/validator.go:Protocols based on AEAD can have additional methods like:
Why do protocols need this? Since all of these protocols are exposed through API's
HandlerService, it requires them to implementproxy.UserManagerinterface. SoValidatorhelps to satisfy this requirement, while also being useful as an authentication backend. You can findproxy.UserManagerimplementation in each of the protocols and all of them are wrappingValidator, e.g. in/proxy/vless/inbound/inbound.go:How is it useful for us? Well, since our goal is to implement custom users storage, there is no better place, than modifying existing
Validator: we don't need to reinvent the wheel, we just need to rewrite storage interaction.💾
Validatorand in-memory storageLet's take a look at
/proxy/vless/validator.goand itsAdd,DelandGetmethods:Since Xray-core exclusively implements only in-memory storage, everything in terms of users storage, basically narrows down to
v.usersandv.emailusage, which aresync.Map:Although, that statement is right only for
VLESSandTrojanprotocols, becauseVMessandShadowSocksare using users list (slice) withsync.RWMutexfor better performance.proxy/vmess/validator.go:proxy/shadowsocks/validator.go:Further notes will follow idea of implementing custom users storage, by creating
sync.Mapcompatible solution, so we can just drag and drop it for protocols likeVLESSandTrojan. It can also be used for any other protocol, but it will require little bit of changes in eachValidator.📦 Users storage implementation with Redis
📋 Preparation
Let's get to creation of users storage compatible with current
sync.Mapmethods used in Xray-core. Redis was picked to build proof of concept, because it's simple and fast. However, you can use any database with binary data support.First of all, we need to install
go-redis:I'm not sure about suitable place for our
storagemodule according to Xray-core structure, so we'll create it undercommonfolder like this/common/storage/. Let's create/common/storage/storage.gowith following contents:This
Storageinterface is literally everything you need to implement. It doesn't include all of thesync.Mapmethods, but it includes all of the methods used acrossValidatorsin Xray-core.We need to move in
/proxy/vless/validator.go, swapsync.Mapwith ourstorage.Storageand create a new method calledNewMemoryValidator(We will use it in injection later, because we can't inject directly inemailandusersfields, since they are private):Also, create base for our
RedisStoragein/common/storage/redis.go:🛠️ Creating an entry point
Take a look at
proxy/vless/inbound/inbound.goand find this block of code:That's the entry point. We need to create two instances of
RedisStorage, one foremailsand one forusers, we will useprefixto separate them. We also need to somehow pass connection string to ourRedisStorage, I'll use environment variables for that.In case you want absolute integration with Xray-core, like ability to configure with JSON, you should modify
/infra/conf/and read fromconfig. Such things aren't covered in this document.Also, don't forget to use
NewMemoryValidatormethod. After all modifications, this code block should look like that:Different protocols and unrelated inbounds should always be separated. Implementation doesn't matter, you can use separate databases, tables, prefixes or any other form of identity, just keep this principle in mind.
👥 Storing and loading users
Remember I said we don't need to reinvent the wheel? In fact, we don't need to invent anything at all! Thanks to all previous developers, we have
/common/protocol/user.go:We can easily make and absolutely should make use of Xray-core obsession with Protocol Buffers. This code may not mean much to you, but take a look at
ToProtousermethod:For a long time, my biggest problem with this project was user serialization and deserialization. I was trying to make use of JSON format, but
Accountwas always getting in my way, because it is unique for each protocol. Which means, you need to know, which exact protocol stores and asksstoragemodule for data. This required me to make some crazy amount of abstractions for eachValidator. And then I found out about Protocol Buffers and their implementation in Xray-core.ToProtoUserfunction serializesAccountto aTypedMessage, meaning it carries not only aValue, but also aType. Thanks to Protocol Buffers, all of the existing types are globally registered in Xray-core, meaningAccountdeserialized fromTypedMessageis automatically comes with rightType. You serializedvless.Account, you will getvless.Accountback on deserialization, it's simple as it is!Marshaling and unmarshaling Protocol Buffers is also pretty simple, there is no difference with JSON at all:
Now, you know basically everything and I'm ready to show you final implementation of
RedisStoragein/common/storage/redis.go:🧪 Let's do some tests
Binaries were built against source code provided above with simple
docker-compose.ymlrunning along:Two configurations were created in order to test our users storage and its ability to share users across multiple instances of Xray-core. One instance had a client to add to
Validatorwith ourstorageand another one, was completely empty.config.json:{ "log": { "loglevel": "info" }, "inbounds": [ { "listen": "0.0.0.0", "port": 1234, "protocol": "vless", "settings": { "clients": [ { "id": "02276781-1fc8-4666-b111-78e7a348b936", "level": 0, "email": "love@example.com" } ], "decryption": "none" }, "streamSettings": { "network": "tcp" } } ], "outbounds": [ { "protocol": "freedom", "tag": "direct" } ] }config2.json:{ "log": { "loglevel": "info" }, "inbounds": [ { "listen": "0.0.0.0", "port": 12345, "protocol": "vless", "settings": { "decryption": "none" }, "streamSettings": { "network": "tcp" } } ], "outbounds": [ { "protocol": "freedom", "tag": "direct" } ] }Here are logs from launch of first instance (IPs are censored intentionally):
And here are logs from launch of second instance at the same time:
Exactly, everything works just as expected! While not having any clients, second instance successfully loaded our user from Redis users storage, which was added there by the first one.
Remember I told you, it will be completely compatible with existing functionality? Well, as far as I tested, it's absolutely is:
💡 What's next?
Use it however you want! You can easily integrate such solution with any existing backend of your own, using Xray-core gRPC API or implement your own way of adding users directly to database, it's not that hard either!
Thanks for reading and hope this helps someone! I would be really happy, if users storages support will be added to Xray-core at some point!
Feel free to ask questions here or even message me, I'm always glad to help!
Beta Was this translation helpful? Give feedback.
All reactions