@alltiptop/geoip-3xui-rules
v0.3.3
Published
Middleware server to set routing rules by countries for XRAY
Maintainers
Readme
3x-ui Country-Based Rules Middleware
This service fetches a JSON subscription from 3x-ui and merges it with country-specific routing rules stored in the rules/ directory.
Examples:
rules/nl.jsonis applied for clients detected in the Netherlandsrules/de.jsonfor Germanyrules/default.jsonwhen no country-specific file exists
Installation
npm
npm i @alltiptop/geoip-3xui-rulesUsage
import { createServer } from 'xui-json-client-rules';
import dotenv from 'dotenv';
dotenv.config();
const app = await createServer({
upstreamUrl: process.env.UPSTREAM_URL!, // URL of your 3x-ui instance
secretUrl: process.env.SECRET_URL!, // Secret path segment to hide the endpoint
directSameCountry: true, // Direct same country as user by ip and domain
rulesDir: 'rules', // Directory containing JSON rules
logger: process.env.NODE_ENV !== 'production'
xuiOptions: { // Opional settings for 3x-ui panel api
panelAddress: process.env.XUI_PANEL_URL, // 3x-ui panel address
username: process.env.XUI_PANEL_LOGIN, // 3x-ui login
password: process.env.XUI_PANEL_PASSWORD, // 3x-ui password
inboundIds: [process.env.XUI_INBOUND_ID], // inbounds list for users
debug: process.env.NODE_ENV !== 'production',
},
// Optional: post-process the final merged JSON before it is sent
transform: (json) => {
// Example: force warning log level and annotate remarks
json.log = json.log || {};
json.log.loglevel = 'warning';
if (typeof json.remarks === 'string') json.remarks = `${json.remarks} [transformed]`;
return json;
},
});
app.listen({ port: 3088, host: '0.0.0.0' });Rule Files
A template is available in the rules-template/ directory. Rule files follow the native Xray routing format: https://xtls.github.io/en/config/routing.html
base.json– applied first, before any country-specific rules.default.json– fallback rules when no matching country file is found.XX.json– any ISO-3166-1 alpha-2 country code, such asde,nl,us, etc.tags/<tag>/base.json– mandatory file, applied first for every visitor who activates this tag.tags/<tag>/default.json– fallback for the tag when there is no country-specific override.tags/<tag>/<ISO>.json– tag override for a particular country (ISO-3166-1 alpha-2). Example:tags/streaming/US.json.
Tag presets (optional)
Tags let you quickly switch additional rule-packs on/off without creating separate subscriptions. A tag becomes active when it is present either in the request URL or in the user’s comment inside 3x-ui:
GET /<secret>/json/<subId>?tags=gaming,streaming ← query parameter (comma list or repeated key)
// 3x-ui › User › comment field
tags=gaming,streaming; another=data ← semicolon/new-line separated, case-insensitiveThe backend merges both sources, removes duplicates, then processes every active tag in the order they were discovered.
Create a directory per tag under rules/tags/. The directory must contain base.json, and optionally default.json plus any number of country overrides:
rules
├ base.json # Global baseline rules (applied first)
├ default.json # Fallback when no country match
├ us.json # Country preset (USA)
├ eu.json # Regional preset (European Union)
├ !de.json # Reverse preset, applies to everyone except DE
├ !eu.json # Reverse preset, applies to everyone NOT in the EU (isEU === false)
└─ tags/
└─ streaming/
├─ base.json # always loaded first
├─ default.json # used when visitor’s country has no override
├─ us.json # overrides for United States
└─ de.json # overrides for GermanyFile loading order per tag (case-insensitive file names):
base.json– always first.XX.json– matching the visitor’s ISO-3166 country code (e.g.us.json).default.json– only when a country file is not found.
This allows you to keep shared logic in base while adding country-specific tweaks only where necessary.
Rule application order (updated)
- Direct rule – Routes requests to
publicURLdirectly to avoid geo-misdetection during self-updates. base.json– Global baseline for everyone.- Tag presets – For every active tag:
base.json→ country override (ordefault.json). - Same-country rules – If
directSameCountryis enabled, traffic destined to the client’s own country goes direct. - Reverse presets (exclude countries) – Files named like
!fr.jsonor!fr,nl,de.json(see below). - Regional preset –
eu.jsonfor EU visitors. - Country preset – Specific country file (e.g.
us.json), ordefault.jsonwhen none exists.
Reusable snippets with "@include"
You can keep common rule fragments in rules/includes/*.json and inline them in any rules file using a special string syntax:
// rules/includes/de-proxy.json
{
"outboundTag": "direct",
"domain": ["domain:de"],
"enabled": true
}Use it from another file (note the quotes around the include token):
// rules/default.json
[
"@include de-proxy",
{
"outboundTag": "proxy",
"ip": ["geoip:fr"],
"enabled": true,
"remarks": "GeoIP FR",
"type": "field"
}
]Details:
- Place files under
rules/includes/. - Syntax:
"@include <name>"or"@include <name>.json". - Includes are expanded recursively; circular references are ignored.
- Missing includes are replaced with
{}(no-op object) to keep JSON valid.
Reverse presets (exclude countries)
Sometimes you want a rule-set to apply to everyone except certain countries. Create files in rules/ whose names start with !:
rules/
├ !fr.json # applies to all visitors whose ISO ≠ FR
├ !fr,nl,de.json # applies to all visitors whose ISO ∉ {FR, NL, DE}
└ !eu.json # applies to all visitors NOT in the EU (special token 'EU')Each file contains a standard array of Xray routing.rules items. At request time, the middleware injects these rules if the visitor’s ISO-3166 country code is not in the exclude list.
Application order: after same-country rules and before regional/country presets (see order above).
Country overrides via query params
You can override the detected country/EU status for testing or custom routing by passing query parameters to the JSON endpoint:
?country=DE&isEU=truecountry– ISO-3166 alpha-2 code (case-insensitive), e.g.de,US.isEU– boolean accepted values:true|false|1|0|yes|no|on|off.
When provided, these values override the result from IP geolocation for the current request only.
Transform hook
You can optionally provide a transform function in createServer options to modify the final JSON right before it is returned to the client.
Signature:
transform?: (
json: Record<string, unknown>,
iso: string,
subId: string,
isEU?: boolean,
) => Record<string, unknown> | Promise<Record<string, unknown>>Notes:
- Runs after merging upstream config, base/country/tag rules, overrides, reverse presets.
- If it throws, the original merged JSON is returned and the error is logged.
- Return the updated object; if you return
undefined, the previous value is used.
Example:
const app = await createServer({
// ...other options
transform: async (json, iso, isEU) => {
// Drop stats section and ensure warning log level
delete (json as any).stats;
json.log = json.log || {};
json.log.loglevel = 'warning';
json.remarks = `${json.remarks || ''} [iso:${iso || '??'} eu:${Boolean(isEU)}]`;
return json;
},
});Why?
3x-ui allows only one set of rules per subscription. This middleware automatically serves different rule sets based on the client’s IP country.
