Compare commits

...

3 commits

Author SHA1 Message Date
Sanaei
c2d6dd923f
windows workflow (#3439)
Some checks are pending
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (push) Waiting to run
2025-09-09 18:41:44 +02:00
mhsanaei
723ec25fb2
renamed dest to target 2025-09-09 14:35:21 +02:00
mhsanaei
7dc52e9a53
dokodemo-door, socks renamed to mixed, tunnel 2025-09-09 13:57:40 +02:00
17 changed files with 143 additions and 54 deletions

View file

@ -146,3 +146,79 @@ jobs:
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
overwrite: true overwrite: true
prerelease: true prerelease: true
# =================================
# Windows Build
# =================================
build-windows:
name: Build for Windows
permissions:
contents: write
strategy:
matrix:
platform:
- amd64
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Build 3X-UI for Windows
shell: pwsh
run: |
$env:CGO_ENABLED="1"
$env:GOOS="windows"
$env:GOARCH="amd64"
go build -ldflags "-w -s" -o xui-release.exe -v main.go
mkdir x-ui
Copy-Item xui-release.exe x-ui\
mkdir x-ui\bin
cd x-ui\bin
# Download Xray for Windows
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.6.8/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip"
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
Rename-Item xray.exe xray-windows-amd64.exe
cd ..
Copy-Item -Path ..\windows_files\* -Destination . -Recurse
cd ..
- name: Package to Zip
shell: pwsh
run: |
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
- name: Upload files to Artifacts
uses: actions/upload-artifact@v4
with:
name: x-ui-windows-amd64
path: ./x-ui-windows-amd64.zip
- name: Upload files to GH release
uses: svenstaro/upload-release-action@v2
if: |
(github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-windows-amd64.zip
asset_name: x-ui-windows-amd64.zip
overwrite: true
prerelease: true

View file

@ -12,11 +12,11 @@ type Protocol string
const ( const (
VMESS Protocol = "vmess" VMESS Protocol = "vmess"
VLESS Protocol = "vless" VLESS Protocol = "vless"
DOKODEMO Protocol = "dokodemo-door" Tunnel Protocol = "tunnel"
HTTP Protocol = "http" HTTP Protocol = "http"
Trojan Protocol = "trojan" Trojan Protocol = "trojan"
Shadowsocks Protocol = "shadowsocks" Shadowsocks Protocol = "shadowsocks"
Socks Protocol = "socks" Mixed Protocol = "mixed"
WireGuard Protocol = "wireguard" WireGuard Protocol = "wireguard"
) )

View file

@ -13,7 +13,7 @@
"inbounds": [ "inbounds": [
{ {
"port": 10808, "port": 10808,
"protocol": "socks", "protocol": "mixed",
"settings": { "settings": {
"auth": "noauth", "auth": "noauth",
"udp": true, "udp": true,
@ -28,7 +28,7 @@
], ],
"enabled": true "enabled": true
}, },
"tag": "socks" "tag": "mixed"
}, },
{ {
"port": 10809, "port": 10809,

View file

@ -49,8 +49,8 @@ class DBInbound {
return this.protocol === Protocols.SHADOWSOCKS; return this.protocol === Protocols.SHADOWSOCKS;
} }
get isSocks() { get isMixed() {
return this.protocol === Protocols.SOCKS; return this.protocol === Protocols.MIXED;
} }
get isHTTP() { get isHTTP() {

View file

@ -3,8 +3,8 @@ const Protocols = {
VLESS: 'vless', VLESS: 'vless',
TROJAN: 'trojan', TROJAN: 'trojan',
SHADOWSOCKS: 'shadowsocks', SHADOWSOCKS: 'shadowsocks',
DOKODEMO: 'dokodemo-door', TUNNEL: 'tunnel',
SOCKS: 'socks', MIXED: 'mixed',
HTTP: 'http', HTTP: 'http',
WIREGUARD: 'wireguard', WIREGUARD: 'wireguard',
}; };
@ -729,7 +729,7 @@ class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false, show = false,
xver = 0, xver = 0,
dest = 'google.com:443', target = 'google.com:443',
serverNames = 'google.com,www.google.com', serverNames = 'google.com,www.google.com',
privateKey = '', privateKey = '',
minClientVer = '', minClientVer = '',
@ -742,7 +742,7 @@ class RealityStreamSettings extends XrayCommonClass {
super(); super();
this.show = show; this.show = show;
this.xver = xver; this.xver = xver;
this.dest = dest; this.target = target;
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames; this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
this.privateKey = privateKey; this.privateKey = privateKey;
this.minClientVer = minClientVer; this.minClientVer = minClientVer;
@ -767,7 +767,7 @@ class RealityStreamSettings extends XrayCommonClass {
return new RealityStreamSettings( return new RealityStreamSettings(
json.show, json.show,
json.xver, json.xver,
json.dest, json.target,
json.serverNames, json.serverNames,
json.privateKey, json.privateKey,
json.minClientVer, json.minClientVer,
@ -783,7 +783,7 @@ class RealityStreamSettings extends XrayCommonClass {
return { return {
show: this.show, show: this.show,
xver: this.xver, xver: this.xver,
dest: this.dest, target: this.target,
serverNames: this.serverNames.split(","), serverNames: this.serverNames.split(","),
privateKey: this.privateKey, privateKey: this.privateKey,
minClientVer: this.minClientVer, minClientVer: this.minClientVer,
@ -1712,8 +1712,8 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol); case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol); case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol); case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol); case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol); case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol); case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
default: return null; default: return null;
@ -1726,8 +1726,8 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json); case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json); case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json); case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json); case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json); case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json); case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
default: return null; default: return null;
@ -2327,7 +2327,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
}; };
Inbound.DokodemoSettings = class extends Inbound.Settings { Inbound.TunnelSettings = class extends Inbound.Settings {
constructor( constructor(
protocol, protocol,
address, address,
@ -2345,8 +2345,8 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.DokodemoSettings( return new Inbound.TunnelSettings(
Protocols.DOKODEMO, Protocols.TUNNEL,
json.address, json.address,
json.port, json.port,
XrayCommonClass.toHeaders(json.portMap), XrayCommonClass.toHeaders(json.portMap),
@ -2366,8 +2366,8 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
} }
}; };
Inbound.SocksSettings = class extends Inbound.Settings { Inbound.MixedSettings = class extends Inbound.Settings {
constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') { constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
super(protocol); super(protocol);
this.auth = auth; this.auth = auth;
this.accounts = accounts; this.accounts = accounts;
@ -2387,11 +2387,11 @@ Inbound.SocksSettings = class extends Inbound.Settings {
let accounts; let accounts;
if (json.auth === 'password') { if (json.auth === 'password') {
accounts = json.accounts.map( accounts = json.accounts.map(
account => Inbound.SocksSettings.SocksAccount.fromJson(account) account => Inbound.MixedSettings.SocksAccount.fromJson(account)
) )
} }
return new Inbound.SocksSettings( return new Inbound.MixedSettings(
Protocols.SOCKS, Protocols.MIXED,
json.auth, json.auth,
accounts, accounts,
json.udp, json.udp,
@ -2408,7 +2408,7 @@ Inbound.SocksSettings = class extends Inbound.Settings {
}; };
} }
}; };
Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass { Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) { constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
super(); super();
this.user = user; this.user = user;
@ -2416,7 +2416,7 @@ Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.SocksSettings.SocksAccount(json.user, json.pass); return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
} }
}; };

View file

@ -6,7 +6,7 @@ const Protocols = {
VLESS: "vless", VLESS: "vless",
Trojan: "trojan", Trojan: "trojan",
Shadowsocks: "shadowsocks", Shadowsocks: "shadowsocks",
Socks: "socks", Mixed: "mixed",
HTTP: "http", HTTP: "http",
Wireguard: "wireguard" Wireguard: "wireguard"
}; };
@ -643,7 +643,7 @@ class Outbound extends CommonClass {
Protocols.Trojan, Protocols.Trojan,
Protocols.Shadowsocks, Protocols.Shadowsocks,
Protocols.HTTP, Protocols.HTTP,
Protocols.Socks Protocols.Mixed
].includes(this.protocol); ].includes(this.protocol);
} }
@ -652,7 +652,7 @@ class Outbound extends CommonClass {
} }
hasServers() { hasServers() {
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol); return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Mixed, Protocols.HTTP].includes(this.protocol);
} }
hasAddressPort() { hasAddressPort() {
@ -662,13 +662,13 @@ class Outbound extends CommonClass {
Protocols.VLESS, Protocols.VLESS,
Protocols.Trojan, Protocols.Trojan,
Protocols.Shadowsocks, Protocols.Shadowsocks,
Protocols.Socks, Protocols.Mixed,
Protocols.HTTP Protocols.HTTP
].includes(this.protocol); ].includes(this.protocol);
} }
hasUsername() { hasUsername() {
return [Protocols.Socks, Protocols.HTTP].includes(this.protocol); return [Protocols.Mixed, Protocols.HTTP].includes(this.protocol);
} }
static fromJson(json = {}) { static fromJson(json = {}) {
@ -847,7 +847,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.VLESS: return new Outbound.VLESSSettings(); case Protocols.VLESS: return new Outbound.VLESSSettings();
case Protocols.Trojan: return new Outbound.TrojanSettings(); case Protocols.Trojan: return new Outbound.TrojanSettings();
case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings(); case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings();
case Protocols.Socks: return new Outbound.SocksSettings(); case Protocols.Mixed: return new Outbound.MixedSettings();
case Protocols.HTTP: return new Outbound.HttpSettings(); case Protocols.HTTP: return new Outbound.HttpSettings();
case Protocols.Wireguard: return new Outbound.WireguardSettings(); case Protocols.Wireguard: return new Outbound.WireguardSettings();
default: return null; default: return null;
@ -863,7 +863,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json); case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json);
case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json); case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json);
case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json); case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json);
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json); case Protocols.Mixed: return Outbound.MixedSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json); case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
default: return null; default: return null;
@ -1141,7 +1141,7 @@ Outbound.ShadowsocksSettings = class extends CommonClass {
} }
}; };
Outbound.SocksSettings = class extends CommonClass { Outbound.MixedSettings = class extends CommonClass {
constructor(address, port, user, pass) { constructor(address, port, user, pass) {
super(); super();
this.address = address; this.address = address;
@ -1153,7 +1153,7 @@ Outbound.SocksSettings = class extends CommonClass {
static fromJson(json = {}) { static fromJson(json = {}) {
let servers = json.servers; let servers = json.servers;
if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }]; if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }];
return new Outbound.SocksSettings( return new Outbound.MixedSettings(
servers[0].address, servers[0].address,
servers[0].port, servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user, ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,

View file

@ -83,14 +83,14 @@
{{template "form/shadowsocks"}} {{template "form/shadowsocks"}}
</template> </template>
<!-- dokodemo-door --> <!-- tunnel -->
<template v-if="inbound.protocol === Protocols.DOKODEMO"> <template v-if="inbound.protocol === Protocols.TUNNEL">
{{template "form/dokodemo"}} {{template "form/tunnel"}}
</template> </template>
<!-- socks --> <!-- mixed -->
<template v-if="inbound.protocol === Protocols.SOCKS"> <template v-if="inbound.protocol === Protocols.MIXED">
{{template "form/socks"}} {{template "form/mixed"}}
</template> </template>
<!-- http --> <!-- http -->

View file

@ -241,9 +241,9 @@
</template> </template>
</template> </template>
<!-- Servers (trojan/shadowsocks/socks/http) settings --> <!-- Servers (trojan/shadowsocks/mixed/http) settings -->
<template v-if="outbound.hasServers()"> <template v-if="outbound.hasServers()">
<!-- http / socks --> <!-- http / mixed -->
<template v-if="outbound.hasUsername()"> <template v-if="outbound.hasUsername()">
<a-form-item label='{{ i18n "username" }}'> <a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="outbound.settings.user"></a-input> <a-input v-model.trim="outbound.settings.user"></a-input>

View file

@ -1,4 +1,4 @@
{{define "form/dokodemo"}} {{define "form/tunnel"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'> <a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
<a-input v-model.trim="inbound.settings.address"></a-input> <a-input v-model.trim="inbound.settings.address"></a-input>

View file

@ -1,4 +1,4 @@
{{define "form/socks"}} {{define "form/mixed"}}
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'> <a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
<a-switch v-model="inbound.settings.udp"></a-switch> <a-switch v-model="inbound.settings.udp"></a-switch>
@ -15,7 +15,7 @@
<td width="45%">{{ i18n "username" }}</td> <td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td> <td width="45%">{{ i18n "password" }}</td>
<td> <td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button> <a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -12,8 +12,8 @@
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Dest (Target)'> <a-form-item label='Target'>
<a-input v-model.trim="inbound.stream.reality.dest"></a-input> <a-input v-model.trim="inbound.stream.reality.target"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='SNI'> <a-form-item label='SNI'>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input> <a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>

View file

@ -354,7 +354,7 @@
<code>[[ link.link ]]</code> <code>[[ link.link ]]</code>
</tr-info-row> </tr-info-row>
</template> </template>
<table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table"> <table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
<tr> <tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th> <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th> <th>{{ i18n "pages.inbounds.destinationPort" }}</th>
@ -376,7 +376,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<table v-if="dbInbound.isSocks" class="tr-info-table"> <table v-if="dbInbound.isMixed" class="tr-info-table">
<tr> <tr>
<th>{{ i18n "password" }} Auth</th> <th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th> <th>{{ i18n "pages.inbounds.enable" }} udp</th>

View file

@ -572,7 +572,7 @@
serverObj = o.settings.vnext; serverObj = o.settings.vnext;
break; break;
case Protocols.HTTP: case Protocols.HTTP:
case Protocols.Socks: case Protocols.Mixed:
case Protocols.Shadowsocks: case Protocols.Shadowsocks:
case Protocols.Trojan: case Protocols.Trojan:
serverObj = o.settings.servers; serverObj = o.settings.servers;

View file

@ -19,7 +19,7 @@
"tag": "api", "tag": "api",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"port": 62789, "port": 62789,
"protocol": "dokodemo-door", "protocol": "tunnel",
"settings": { "settings": {
"address": "127.0.0.1" "address": "127.0.0.1"
} }

View file

@ -2129,8 +2129,8 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
} }
excludedProtocols := map[model.Protocol]bool{ excludedProtocols := map[model.Protocol]bool{
model.DOKODEMO: true, model.Tunnel: true,
model.Socks: true, model.Mixed: true,
model.WireGuard: true, model.WireGuard: true,
model.HTTP: true, model.HTTP: true,
} }

Binary file not shown.

13
windows_files/readme.txt Normal file
View file

@ -0,0 +1,13 @@
you can't install fail2ban on windows
we don't have bash menu for windows
if you forgot your password you need to check your database with https://sqlitebrowser.org/
the app need to be open all the time
default setting:
http://localhost:2053/
user: admin
pass: admin
port: 2053
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt