Server-Side Routing
This guide is meant purely as a demonstration of server-side routing. Do not assume that copying these examples will produce a fully functional setup in a production. It does not cover all capabilities of Xray-core, nor is it guaranteed to work perfectly in every environment.
For full details on configuration syntax and behavior, refer to the official Xray documentation.
Some users may have noticed that Remnawave automatically clears the clients array from the Node's configuration. This is not a bug, but rather an intended behavior. Remnawave clears it to ensure that only necessary configurations are retained for certain features to function properly.
In this guide, we'll walk through setting up server-side traffic routing using two Nodes: RU and DE. Users will connect to the RU Node. From there, the traffic will be routed as follows:
- Traffic to
.ruwebsites will be proxied directly through theRUNode. - All other traffic will be routed through the
DENode.
To achieve this, we'll create a service user that will facilitate the traffic routing.
As a result, the traffic "flow" could look something like this:
- The user opens
google.com. - The request first reaches the
RUNode. - The
RUNode applies the routing rules. Sincegoogle.comis not a.rudomain, the traffic is routed to theDENode. - The
DENode completes the connection togoogle.com.
This type of server-side routing setup is often referred to as a "bridge".
Create a Config Profile for DEβ
Navigate to the Config Profiles section and create a new Config Profile, e.g. DE Bridge Profile.
{
"log": {
"loglevel": "warning"
},
"dns": {},
"inbounds": [
{
"tag": "BRIDGE_DE_IN",
"port": 9999,
"listen": "0.0.0.0",
"protocol": "shadowsocks",
"settings": {
"clients": [],
"network": "tcp,udp"
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
}
}
],
"outbounds": [
{
"tag": "DIRECT",
"protocol": "freedom"
},
{
"tag": "BLOCK",
"protocol": "blackhole"
}
],
"routing": {
"rules": []
}
}
Assign the newly created Config Profile to the DE Node. Select the BRIDGE_DE_IN Inbound.
Create a new Internal Squad, let's call it Bridge Squad. Enable the Inbound we created earlier β BRIDGE_DE_IN.
Create a Service Userβ
For our bridge to work, we will need to create a user. Let's call it bridge_user.
Since this is a service user, we obviously don't want its subscription to expire or be limited by traffic usage. Set the Data Limit to 0 and Expiry Date to year 2099.
Next, assign the appropriate Squad to this user. In our case, that's Bridge Squad.
After the service user is created, we will need to get its password. Open the user card, click on the More Actions button, then Detailed Info. There you should see Connection Information section.
Depending on the Inbound's protocol (Shadowsocks in our case), copy the appropriate value:
| Protocol | Connection Info Value |
|---|---|
| Trojan | Trojan Password |
| VLESS | VLESS UUID |
| Shadowsocks | SS Password |
Configure a Public Profileβ
Most likely, you already have a Config Profile that your users connect to. If not, refer to this guide.
For this step, we're not interested in the "inbounds" β instead, we'll be modifying the "outbounds", "routing", and "rules" arrays of that Profile.
{
"log": {
"loglevel": "none"
},
"inbounds": [
{
"tag": "PUBLIC_RU_INBOUND",
"port": 443,
"listen": "0.0.0.0",
"protocol": "vless",
"settings": {
"clients": [],
"decryption": "none"
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
},
"streamSettings": {
"network": "raw",
"security": "reality",
"realitySettings": {
"target": "USE YOUR OWN VALUE!",
"show": false,
"xver": 0,
"shortIds": [""],
"privateKey": "USE YOUR OWN KEY!",
"serverNames": ["USE YOUR OWN VALUES!"]
}
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "DIRECT"
},
{
"protocol": "blackhole",
"tag": "BLOCK"
}
],
"routing": {
"rules": [
{
"ip": ["geoip:private"],
"outboundTag": "BLOCK"
},
{
"domain": ["geosite:private"],
"outboundTag": "BLOCK"
},
{
"protocol": ["bittorrent"],
"outboundTag": "BLOCK"
}
]
}
}
Outboundsβ
First we need to modify the outbounds array.
{
"tag": "SS_OUTBOUND_TO_DE",
"protocol": "shadowsocks",
"settings": {
"servers": [
{
"address": "DE NODE ADDRESS",
"email": "bridge_user",
"password": "PASSWORD FROM PREVIOUS STEP",
"port": 9999,
"level": 0,
"method": "chacha20-ietf-poly1305"
}
]
}
}
| Field | Value |
|---|---|
address | The IP address or domain name of your DE Node server. |
port | The Inbound port, in our case the port of BRIDGE_DE_IN. |
email | The username of the service user you created. In our case, bridge_user. |
password | The password for the service user. Depends on the protocol you are using; see the previous step. |
method | Encryption method. For Shadowsocks in Remnawave, always use chacha20-ietf-poly1305. |
Routing and Rulesβ
Next, modify the routing rules according to your needs.
Below is the example of a rule that makes the RU Node an exit node for Russian IPs and websites, while the rest of the traffic gets sent to DE Node.
{
"ip": ["geoip:ru"],
"outboundTag": "DIRECT"
},
{
"domain": ["geosite:category-ru"],
"outboundTag": "DIRECT"
}
Below is the example of a rule that proxies all traffic to SS_OUTBOUND_TO_DE Outbound.
{
"inboundTag": ["PUBLIC_RU_INBOUND"],
"outboundTag": "SS_OUTBOUND_TO_DE"
}
The complete public Config Profile could look like this:
{
"log": {
"loglevel": "none"
},
"inbounds": [
{
"tag": "PUBLIC_RU_INBOUND",
"port": 443,
"listen": "0.0.0.0",
"protocol": "vless",
"settings": {
"clients": [],
"decryption": "none"
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls", "quic"]
},
"streamSettings": {
"network": "raw",
"security": "reality",
"realitySettings": {
"target": "USE YOUR OWN VALUE!",
"show": false,
"xver": 0,
"shortIds": [""],
"privateKey": "USE YOUR OWN KEY!",
"serverNames": ["USE YOUR OWN VALUES!"]
}
}
}
],
"outbounds": [
{
"protocol": "freedom",
"tag": "DIRECT"
},
{
"protocol": "blackhole",
"tag": "BLOCK"
},
{
"tag": "SS_OUTBOUND_TO_DE",
"protocol": "shadowsocks",
"settings": {
"servers": [
{
"address": "DE NODE ADDRESS",
"email": "bridge_user",
"password": "PASSWORD FROM PREVIOUS STEP",
"port": 9999,
"level": 0,
"method": "chacha20-ietf-poly1305"
}
]
}
}
],
"routing": {
"rules": [
{
"ip": ["geoip:private"],
"outboundTag": "BLOCK"
},
{
"domain": ["geosite:private"],
"outboundTag": "BLOCK"
},
{
"protocol": ["bittorrent"],
"outboundTag": "BLOCK"
},
{
"ip": ["geoip:ru"],
"outboundTag": "DIRECT"
},
{
"domain": ["geosite:category-ru"],
"outboundTag": "DIRECT"
},
{
"inboundTag": ["PUBLIC_RU_INBOUND"],
"outboundTag": "SS_OUTBOUND_TO_DE"
}
]
}
}
Xray routing rules are processed in order, from top to bottom.
Here's how the rules in our example work:
-
Private IPs (
geoip:private) βBLOCK
Traffic matching this rule is sent to theBLOCKOutbound, which is ablackholeprotocol (completely blocked). -
Private domains (
geosite:private) βBLOCK
Same as above, but for domain names. -
Bittorrent traffic (
bittorrent) βBLOCK
All BitTorrent traffic is blocked. -
Russian IPs (
geoip:ru) βDIRECT
Traffic matching this rule is sent to theDIRECTOutbound, which is afreedomprotocol (RUNode acts as an exit node). -
Russian domains (
geosite:category-ru) βDIRECT
Traffic to Russian domains also goes directly out from the RU Node. -
No rule match (the rest of the traffic) β
SS_OUTBOUND_TO_DE
Any remaining traffic arriving at thePUBLIC_RU_INBOUNDis routed to theDENode via theShadowsocksOutbound we created earlier.
You don't have to use Shadowsocks as the transit protocol β you can use VLESS instead. Just make sure to update your configuration accordingly.