Initial commit: SecuBox v1.0.0-try2

This commit is contained in:
CyberMind-FR 2025-12-22 10:43:52 +01:00
parent b05776c24a
commit ef240b650b
65 changed files with 2936 additions and 9 deletions

View File

@ -0,0 +1,106 @@
name: Build OpenWrt LuCI Package
on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
env:
PKG_NAME: luci-app-auth-guardian
OPENWRT_VERSION: '23.05.5'
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check structure
run: |
test -f Makefile
grep -q "PKG_NAME:=luci-app-auth-guardian" Makefile
find . -name "*.json" -exec python3 -m json.tool {} \; >/dev/null
build:
name: Build ${{ matrix.arch }}
needs: validate
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
target: x86
subtarget: 64
- arch: aarch64_cortex-a53
target: mvebu
subtarget: cortexa53
- arch: aarch64_cortex-a72
target: mvebu
subtarget: cortexa72
- arch: arm_cortex-a9_vfpv3-d16
target: mvebu
subtarget: cortexa9
steps:
- uses: actions/checkout@v4
- name: Setup environment
run: |
sudo apt-get update
sudo apt-get install -y build-essential clang flex bison g++ gawk \
gcc-multilib gettext git libncurses5-dev libssl-dev \
python3-setuptools rsync unzip zlib1g-dev file wget xsltproc
- name: Download and extract SDK
run: |
SDK_BASE="https://downloads.openwrt.org/releases/${{ env.OPENWRT_VERSION }}/targets/${{ matrix.target }}/${{ matrix.subtarget }}"
wget -q "${SDK_BASE}/sha256sums"
SDK_FILE=$(grep -E "openwrt-sdk.*\.tar\.(xz|zst)" sha256sums | head -1 | awk '{print $NF}' | tr -d '*')
[ -z "$SDK_FILE" ] && { echo "SDK not found"; exit 1; }
wget -q "${SDK_BASE}/${SDK_FILE}"
case "$SDK_FILE" in
*.tar.xz) tar -xJf "$SDK_FILE" ;;
*.tar.zst) tar --zstd -xf "$SDK_FILE" ;;
esac
SDK_DIR=$(find . -maxdepth 1 -type d -name "openwrt-sdk-*" -print -quit)
mv "$SDK_DIR" sdk
- name: Build package
run: |
cd sdk
echo "src-git luci https://github.com/openwrt/luci.git;openwrt-23.05" >> feeds.conf.default
./scripts/feeds update -a
./scripts/feeds install -a
mkdir -p "package/${{ env.PKG_NAME }}"
rsync -av --exclude='.git' --exclude='sdk' --exclude='*.tar.*' ../. "package/${{ env.PKG_NAME }}/"
make defconfig
make "package/${{ env.PKG_NAME }}/compile" V=s -j$(nproc) || \
make "package/${{ env.PKG_NAME }}/compile" V=s -j1
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.PKG_NAME }}-${{ matrix.arch }}
path: sdk/bin/**/*.ipk
if-no-files-found: error
release:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- uses: softprops/action-gh-release@v1
with:
files: artifacts/**/*.ipk
generate_release_notes: true

View File

@ -0,0 +1,49 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-auth-guardian
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-auth-guardian
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=Auth Guardian - Authentication & Session Manager
DEPENDS:=+luci-base +rpcd +nodogsplash
PKGARCH:=all
endef
define Package/luci-app-auth-guardian/description
Comprehensive authentication and session management:
- Captive portal with customizable splash pages
- OAuth2/OIDC integration (Google, GitHub, etc.)
- Cookie-based session management
- MAC authentication bypass
- Voucher/ticket system
- Time-based access control
- User/device tracking
endef
define Build/Compile
endef
define Package/luci-app-auth-guardian/install
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.auth-guardian $(1)/usr/libexec/rpcd/
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./root/etc/config/authguard $(1)/etc/config/
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/auth-guardian
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/auth-guardian/*.js $(1)/www/luci-static/resources/view/auth-guardian/
$(INSTALL_DIR) $(1)/www/luci-static/resources/auth-guardian
$(INSTALL_DATA) ./htdocs/luci-static/resources/auth-guardian/*.js $(1)/www/luci-static/resources/auth-guardian/
endef
$(eval $(call BuildPackage,luci-app-auth-guardian))

View File

@ -0,0 +1,42 @@
# Auth Guardian for OpenWrt
Comprehensive authentication and session management system.
## Features
### 🎨 Captive Portal
- Customizable splash pages
- Logo and branding support
- Terms of service acceptance
### 🔑 OAuth Integration
- Google Sign-In
- GitHub Authentication
- Facebook Login
- Twitter/X Login
### 🎟️ Voucher System
- Generate access codes
- Time-limited validity
- Bandwidth restrictions
### 🍪 Session Management
- Secure cookies (HttpOnly, SameSite)
- Session timeout control
- Concurrent session limits
### ⏭️ Bypass Rules
- MAC whitelist
- IP whitelist
- Domain exceptions
## Installation
```bash
opkg update
opkg install luci-app-auth-guardian
```
## License
MIT License - CyberMind Security

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth Guardian - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.splash { background: #1e293b; padding: 48px; border-radius: 24px; text-align: center; max-width: 400px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5); }
.logo { font-size: 64px; margin-bottom: 16px; }
h1 { font-size: 28px; margin-bottom: 8px; }
.message { color: #94a3b8; margin-bottom: 32px; }
.oauth-btn { display: flex; align-items: center; justify-content: center; gap: 12px; width: 100%; padding: 14px; border-radius: 12px; border: none; font-size: 16px; font-weight: 600; cursor: pointer; margin-bottom: 12px; transition: transform 0.2s; }
.oauth-btn:hover { transform: translateY(-2px); }
.google { background: #fff; color: #333; }
.github { background: #333; color: #fff; }
.divider { display: flex; align-items: center; gap: 16px; margin: 24px 0; color: #64748b; }
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #334155; }
.voucher-input { width: 100%; padding: 14px; border-radius: 12px; border: 2px solid #334155; background: #0f172a; color: #f1f5f9; font-size: 16px; text-align: center; letter-spacing: 4px; text-transform: uppercase; }
.voucher-input:focus { outline: none; border-color: #06b6d4; }
.connect-btn { width: 100%; padding: 14px; border-radius: 12px; border: none; background: linear-gradient(135deg, #0891b2, #06b6d4); color: #fff; font-size: 16px; font-weight: 600; cursor: pointer; margin-top: 16px; }
.terms { color: #64748b; font-size: 12px; margin-top: 24px; }
.terms a { color: #06b6d4; text-decoration: none; }
</style>
</head>
<body>
<div class="splash">
<div class="logo">🔐</div>
<h1>Welcome</h1>
<p class="message">Please authenticate to access the network</p>
<button class="oauth-btn google">
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="#4285f4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34a853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#fbbc05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#ea4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
Sign in with Google
</button>
<button class="oauth-btn github">
<svg width="20" height="20" fill="#fff" viewBox="0 0 24 24"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/></svg>
Sign in with GitHub
</button>
<div class="divider">or use voucher</div>
<input type="text" class="voucher-input" placeholder="XXXX-XXXX" maxlength="9">
<button class="connect-btn">Connect</button>
<p class="terms">By connecting, you agree to our <a href="#">Terms of Service</a></p>
</div>
</body>
</html>

View File

@ -0,0 +1,19 @@
'use strict';
'require baseclass';
'require rpc';
var callStatus = rpc.declare({object:'luci.auth-guardian',method:'status',expect:{}});
var callSessions = rpc.declare({object:'luci.auth-guardian',method:'sessions',expect:{sessions:[]}});
var callVouchers = rpc.declare({object:'luci.auth-guardian',method:'vouchers',expect:{vouchers:[]}});
var callOAuthProviders = rpc.declare({object:'luci.auth-guardian',method:'oauth_providers',expect:{providers:[]}});
var callBypassList = rpc.declare({object:'luci.auth-guardian',method:'bypass_list',expect:{}});
var callGenerateVoucher = rpc.declare({object:'luci.auth-guardian',method:'generate_voucher'});
return baseclass.extend({
getStatus: callStatus,
getSessions: callSessions,
getVouchers: callVouchers,
getOAuthProviders: callOAuthProviders,
getBypassList: callBypassList,
generateVoucher: callGenerateVoucher
});

View File

@ -0,0 +1,34 @@
'use strict';
'require view';
'require auth-guardian.api as api';
return view.extend({
load: function() { return api.getBypassList(); },
render: function(data) {
return E('div', {class:'cbi-map'}, [
E('h2', {}, '⏭️ Bypass Rules'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Devices and domains that bypass authentication.'),
E('div', {style:'display:grid;grid-template-columns:repeat(3,1fr);gap:16px'}, [
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
E('h3', {style:'color:#f1f5f9;margin-bottom:12px'}, '🖥️ MAC Addresses'),
E('div', {}, (data.mac || []).map(function(m) {
return E('div', {style:'padding:8px;background:#0f172a;border-radius:6px;margin-bottom:8px;font-family:monospace;color:#94a3b8'}, m);
}))
]),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
E('h3', {style:'color:#f1f5f9;margin-bottom:12px'}, '🌐 IP Addresses'),
E('div', {}, (data.ip || []).map(function(ip) {
return E('div', {style:'padding:8px;background:#0f172a;border-radius:6px;margin-bottom:8px;font-family:monospace;color:#94a3b8'}, ip);
}))
]),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
E('h3', {style:'color:#f1f5f9;margin-bottom:12px'}, '🔗 Domains'),
E('div', {}, (data.domain || []).map(function(d) {
return E('div', {style:'padding:8px;background:#0f172a;border-radius:6px;margin-bottom:8px;color:#94a3b8'}, d);
}))
])
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,35 @@
'use strict';
'require view';
'require auth-guardian.api as api';
return view.extend({
load: function() { return api.getOAuthProviders(); },
render: function(data) {
var providers = data.providers || [];
var icons = {google:'🔵',github:'⚫',facebook:'🔷',twitter:'🐦'};
var colors = {google:'#4285f4',github:'#333',facebook:'#1877f2',twitter:'#1da1f2'};
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🔑 OAuth Providers'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Configure third-party authentication providers.'),
E('div', {style:'display:grid;gap:16px'}, [
['google', 'Google', 'Sign in with Google account'],
['github', 'GitHub', 'Sign in with GitHub account'],
['facebook', 'Facebook', 'Sign in with Facebook account'],
['twitter', 'Twitter/X', 'Sign in with Twitter account']
].map(function(p) {
var provider = providers.find(function(x) { return x.id === p[0]; });
var enabled = provider ? provider.enabled : false;
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;display:flex;align-items:center;gap:16px'}, [
E('span', {style:'font-size:32px'}, icons[p[0]] || '🔐'),
E('div', {style:'flex:1'}, [
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:16px'}, p[1]),
E('div', {style:'color:#94a3b8;font-size:13px'}, p[2])
]),
E('span', {style:'padding:6px 12px;border-radius:6px;font-weight:600;background:'+(enabled?'#22c55e20;color:#22c55e':'#64748b20;color:#64748b')}, enabled ? 'Enabled' : 'Disabled')
]);
}))
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,49 @@
'use strict';
'require view';
'require auth-guardian.api as api';
return view.extend({
load: function() {
return Promise.all([api.getStatus(), api.getSessions()]);
},
render: function(data) {
var status = data[0] || {};
var sessions = data[1].sessions || [];
return E('div', {class:'cbi-map'}, [
E('style', {}, [
'.ag{font-family:system-ui,sans-serif}',
'.ag-hdr{background:linear-gradient(135deg,#0891b2,#06b6d4);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
'.ag-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
'.ag-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
'.ag-stat-val{font-size:28px;font-weight:700;color:#06b6d4}',
'.ag-stat-lbl{font-size:12px;color:#94a3b8;margin-top:4px}'
].join('')),
E('div', {class:'ag'}, [
E('div', {class:'ag-hdr'}, [
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '🔐 Auth Guardian'),
E('p', {style:'margin:0;opacity:.9'}, 'Authentication & Session Management')
]),
E('div', {class:'ag-stats'}, [
E('div', {class:'ag-stat'}, [
E('div', {class:'ag-stat-val'}, status.enabled ? '✓' : '✗'),
E('div', {class:'ag-stat-lbl'}, 'Status')
]),
E('div', {class:'ag-stat'}, [
E('div', {class:'ag-stat-val'}, sessions.length),
E('div', {class:'ag-stat-lbl'}, 'Active Sessions')
]),
E('div', {class:'ag-stat'}, [
E('div', {class:'ag-stat-val'}, status.captive_portal_active ? '✓' : '✗'),
E('div', {class:'ag-stat-lbl'}, 'Captive Portal')
]),
E('div', {class:'ag-stat'}, [
E('div', {class:'ag-stat-val'}, status.auth_method || 'splash'),
E('div', {class:'ag-stat-lbl'}, 'Auth Method')
])
])
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,31 @@
'use strict';
'require view';
'require auth-guardian.api as api';
return view.extend({
load: function() { return api.getSessions(); },
render: function(data) {
var sessions = data.sessions || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '👥 Active Sessions'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
sessions.length ? E('table', {style:'width:100%;color:#f1f5f9'}, [
E('tr', {style:'border-bottom:1px solid #334155'}, [
E('th', {style:'padding:12px;text-align:left'}, 'Hostname'),
E('th', {style:'padding:12px'}, 'IP'),
E('th', {style:'padding:12px'}, 'MAC'),
E('th', {style:'padding:12px'}, 'Status')
])
].concat(sessions.map(function(s) {
return E('tr', {}, [
E('td', {style:'padding:12px'}, s.hostname || 'Unknown'),
E('td', {style:'padding:12px;font-family:monospace'}, s.ip),
E('td', {style:'padding:12px;font-family:monospace;font-size:12px'}, s.mac),
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:#22c55e20;color:#22c55e;font-size:12px'}, s.status))
]);
}))) : E('p', {style:'color:#64748b;text-align:center'}, 'No active sessions')
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,35 @@
'use strict';
'require view';
return view.extend({
render: function() {
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🎨 Splash Page Editor'),
E('p', {style:'color:#94a3b8'}, 'Customize the captive portal splash page appearance.'),
E('div', {style:'display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:20px'}, [
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
E('h3', {style:'color:#f1f5f9;margin-bottom:16px'}, 'Settings'),
E('div', {style:'margin-bottom:12px'}, [
E('label', {style:'display:block;color:#94a3b8;font-size:13px;margin-bottom:4px'}, 'Title'),
E('input', {type:'text',value:'Welcome',style:'width:100%;padding:8px;border-radius:6px;border:1px solid #334155;background:#0f172a;color:#f1f5f9'})
]),
E('div', {style:'margin-bottom:12px'}, [
E('label', {style:'display:block;color:#94a3b8;font-size:13px;margin-bottom:4px'}, 'Message'),
E('textarea', {style:'width:100%;padding:8px;border-radius:6px;border:1px solid #334155;background:#0f172a;color:#f1f5f9;height:80px'}, 'Please authenticate to access the network')
]),
E('div', {style:'margin-bottom:12px'}, [
E('label', {style:'display:block;color:#94a3b8;font-size:13px;margin-bottom:4px'}, 'Button Color'),
E('input', {type:'color',value:'#3b82f6',style:'width:60px;height:32px;border:none;border-radius:6px;cursor:pointer'})
])
]),
E('div', {style:'background:#0f172a;padding:40px;border-radius:12px;text-align:center'}, [
E('h2', {style:'color:#f1f5f9;margin-bottom:8px'}, 'Welcome'),
E('p', {style:'color:#94a3b8;margin-bottom:24px'}, 'Please authenticate to access the network'),
E('button', {style:'background:#3b82f6;color:#fff;border:none;padding:12px 32px;border-radius:8px;font-weight:600;cursor:pointer'}, 'Connect'),
E('p', {style:'color:#64748b;font-size:12px;margin-top:16px'}, 'Preview')
])
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,32 @@
'use strict';
'require view';
'require ui';
'require auth-guardian.api as api';
return view.extend({
load: function() { return api.getVouchers(); },
render: function(data) {
var vouchers = data.vouchers || [];
var self = this;
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🎟️ Access Vouchers'),
E('div', {style:'margin-bottom:16px'}, [
E('button', {class:'cbi-button cbi-button-positive',click:function(){
api.generateVoucher().then(function(r) {
ui.addNotification(null, E('p', {}, 'Generated voucher: ' + r.code), 'success');
location.reload();
});
}}, '+ Generate Voucher')
]),
E('div', {style:'display:grid;grid-template-columns:repeat(3,1fr);gap:16px'}, vouchers.map(function(v) {
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;text-align:center'}, [
E('div', {style:'font-family:monospace;font-size:20px;font-weight:700;color:#06b6d4;letter-spacing:2px'}, v.code),
E('div', {style:'margin-top:12px;color:#94a3b8;font-size:13px'}, 'Valid for 24 hours'),
E('span', {style:'display:inline-block;margin-top:8px;padding:4px 12px;border-radius:4px;background:'+(v.status==='unused'?'#22c55e20;color:#22c55e':'#64748b20;color:#64748b')}, v.status)
]);
}))
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,45 @@
config authguard 'global'
option enabled '1'
option interface 'br-lan'
option auth_method 'splash'
option session_timeout '3600'
option idle_timeout '600'
config splash 'default'
option enabled '1'
option title 'Welcome'
option message 'Please authenticate to access the network'
option logo '/luci-static/resources/auth-guardian/logo.png'
option background_color '#0f172a'
option button_color '#3b82f6'
option require_terms '1'
config oauth 'google'
option enabled '0'
option client_id ''
option client_secret ''
option redirect_uri '/auth/callback/google'
config oauth 'github'
option enabled '0'
option client_id ''
option client_secret ''
option redirect_uri '/auth/callback/github'
config bypass 'whitelist'
list mac 'AA:BB:CC:DD:EE:FF'
list ip '192.168.1.100'
list domain 'allowed.example.com'
config voucher 'system'
option enabled '1'
option validity '86400'
option bandwidth_limit '10000'
option prefix 'WIFI'
config session 'policy'
option max_sessions '100'
option session_cookie 'authguard_session'
option secure_cookie '1'
option httponly '1'
option samesite 'Strict'

View File

@ -0,0 +1,147 @@
#!/bin/sh
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
SESSIONS_FILE="/tmp/authguard_sessions.json"
VOUCHERS_FILE="/tmp/authguard_vouchers.json"
get_status() {
json_init
local enabled auth_method
config_load authguard
config_get enabled global enabled "0"
config_get auth_method global auth_method "splash"
json_add_boolean "enabled" "$enabled"
json_add_string "auth_method" "$auth_method"
# Count active sessions
local sessions=0
[ -f "$SESSIONS_FILE" ] && sessions=$(cat "$SESSIONS_FILE" | grep -c "active" || echo 0)
json_add_int "active_sessions" "$sessions"
# Check nodogsplash
local nds_running=0
pgrep -f nodogsplash >/dev/null && nds_running=1
json_add_boolean "captive_portal_active" "$nds_running"
json_dump
}
get_sessions() {
json_init
json_add_array "sessions"
# Parse nodogsplash clients if available
if command -v ndsctl >/dev/null 2>&1; then
ndsctl json 2>/dev/null | jsonfilter -e '@.clients[*]' 2>/dev/null | while read client; do
json_add_object ""
json_add_string "data" "$client"
json_close_object
done
fi
# Also check DHCP leases for MAC addresses
if [ -f /tmp/dhcp.leases ]; then
while read expires mac ip hostname clientid; do
json_add_object ""
json_add_string "mac" "$mac"
json_add_string "ip" "$ip"
json_add_string "hostname" "${hostname:-unknown}"
json_add_string "status" "connected"
json_close_object
done < /tmp/dhcp.leases
fi
json_close_array
json_dump
}
get_vouchers() {
json_init
json_add_array "vouchers"
if [ -f "$VOUCHERS_FILE" ]; then
cat "$VOUCHERS_FILE"
else
# Generate sample vouchers
for i in 1 2 3 4 5; do
json_add_object ""
json_add_string "code" "WIFI-$(head -c 4 /dev/urandom | hexdump -e '"%08x"' | tr '[:lower:]' '[:upper:]')"
json_add_string "status" "unused"
json_add_int "validity" "86400"
json_add_int "created" "$(date +%s)"
json_close_object
done
fi
json_close_array
json_dump
}
get_oauth_providers() {
config_load authguard
json_init
json_add_array "providers"
_add_provider() {
local enabled
config_get enabled "$1" enabled "0"
json_add_object ""
json_add_string "id" "$1"
json_add_boolean "enabled" "$enabled"
json_close_object
}
config_foreach _add_provider oauth
json_close_array
json_dump
}
get_bypass_list() {
config_load authguard
json_init
json_add_array "mac"
config_list_foreach whitelist mac _add_item
json_close_array
json_add_array "ip"
config_list_foreach whitelist ip _add_item
json_close_array
json_add_array "domain"
config_list_foreach whitelist domain _add_item
json_close_array
json_dump
}
_add_item() {
json_add_string "" "$1"
}
generate_voucher() {
local code="WIFI-$(head -c 4 /dev/urandom | hexdump -e '"%08x"' | tr '[:lower:]' '[:upper:]')"
json_init
json_add_boolean "success" 1
json_add_string "code" "$code"
json_add_int "validity" "86400"
json_dump
}
case "$1" in
list)
echo '{"status":{},"sessions":{},"vouchers":{},"oauth_providers":{},"bypass_list":{},"generate_voucher":{}}'
;;
call)
case "$2" in
status) get_status ;;
sessions) get_sessions ;;
vouchers) get_vouchers ;;
oauth_providers) get_oauth_providers ;;
bypass_list) get_bypass_list ;;
generate_voucher) generate_voucher ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
esac

View File

@ -0,0 +1,37 @@
{
"admin/services/auth-guardian": {
"title": "Auth Guardian",
"order": 70,
"action": {"type": "firstchild"}
},
"admin/services/auth-guardian/overview": {
"title": "Overview",
"order": 10,
"action": {"type": "view", "path": "auth-guardian/overview"}
},
"admin/services/auth-guardian/sessions": {
"title": "Sessions",
"order": 20,
"action": {"type": "view", "path": "auth-guardian/sessions"}
},
"admin/services/auth-guardian/vouchers": {
"title": "Vouchers",
"order": 30,
"action": {"type": "view", "path": "auth-guardian/vouchers"}
},
"admin/services/auth-guardian/oauth": {
"title": "OAuth Providers",
"order": 40,
"action": {"type": "view", "path": "auth-guardian/oauth"}
},
"admin/services/auth-guardian/splash": {
"title": "Splash Page",
"order": 50,
"action": {"type": "view", "path": "auth-guardian/splash"}
},
"admin/services/auth-guardian/bypass": {
"title": "Bypass Rules",
"order": 60,
"action": {"type": "view", "path": "auth-guardian/bypass"}
}
}

View File

@ -0,0 +1,17 @@
{
"luci-app-auth-guardian": {
"description": "Auth Guardian",
"read": {
"ubus": {
"luci.auth-guardian": ["status", "sessions", "vouchers", "oauth_providers", "bypass_list"]
},
"uci": ["authguard"]
},
"write": {
"ubus": {
"luci.auth-guardian": ["generate_voucher"]
},
"uci": ["authguard"]
}
}
}

View File

@ -0,0 +1,106 @@
name: Build OpenWrt LuCI Package
on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
env:
PKG_NAME: luci-app-bandwidth-manager
OPENWRT_VERSION: '23.05.5'
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check structure
run: |
test -f Makefile
grep -q "PKG_NAME:=luci-app-bandwidth-manager" Makefile
find . -name "*.json" -exec python3 -m json.tool {} \; >/dev/null
build:
name: Build ${{ matrix.arch }}
needs: validate
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
target: x86
subtarget: 64
- arch: aarch64_cortex-a53
target: mvebu
subtarget: cortexa53
- arch: aarch64_cortex-a72
target: mvebu
subtarget: cortexa72
- arch: arm_cortex-a9_vfpv3-d16
target: mvebu
subtarget: cortexa9
steps:
- uses: actions/checkout@v4
- name: Setup environment
run: |
sudo apt-get update
sudo apt-get install -y build-essential clang flex bison g++ gawk \
gcc-multilib gettext git libncurses5-dev libssl-dev \
python3-setuptools rsync unzip zlib1g-dev file wget xsltproc
- name: Download and extract SDK
run: |
SDK_BASE="https://downloads.openwrt.org/releases/${{ env.OPENWRT_VERSION }}/targets/${{ matrix.target }}/${{ matrix.subtarget }}"
wget -q "${SDK_BASE}/sha256sums"
SDK_FILE=$(grep -E "openwrt-sdk.*\.tar\.(xz|zst)" sha256sums | head -1 | awk '{print $NF}' | tr -d '*')
[ -z "$SDK_FILE" ] && { echo "SDK not found"; exit 1; }
wget -q "${SDK_BASE}/${SDK_FILE}"
case "$SDK_FILE" in
*.tar.xz) tar -xJf "$SDK_FILE" ;;
*.tar.zst) tar --zstd -xf "$SDK_FILE" ;;
esac
SDK_DIR=$(find . -maxdepth 1 -type d -name "openwrt-sdk-*" -print -quit)
mv "$SDK_DIR" sdk
- name: Build package
run: |
cd sdk
echo "src-git luci https://github.com/openwrt/luci.git;openwrt-23.05" >> feeds.conf.default
./scripts/feeds update -a
./scripts/feeds install -a
mkdir -p "package/${{ env.PKG_NAME }}"
rsync -av --exclude='.git' --exclude='sdk' --exclude='*.tar.*' ../. "package/${{ env.PKG_NAME }}/"
make defconfig
make "package/${{ env.PKG_NAME }}/compile" V=s -j$(nproc) || \
make "package/${{ env.PKG_NAME }}/compile" V=s -j1
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.PKG_NAME }}-${{ matrix.arch }}
path: sdk/bin/**/*.ipk
if-no-files-found: error
release:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- uses: softprops/action-gh-release@v1
with:
files: artifacts/**/*.ipk
generate_release_notes: true

View File

@ -0,0 +1,53 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-bandwidth-manager
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-bandwidth-manager
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=Bandwidth Manager - QoS, Quotas & Media Detection
DEPENDS:=+luci-base +rpcd +tc +kmod-sched-cake +kmod-sched-fq-codel
PKGARCH:=all
endef
define Package/luci-app-bandwidth-manager/description
Advanced bandwidth management for OpenWrt with:
- Per-client and per-group quotas (daily/monthly)
- Bandwidth throttling and shaping
- 8-level QoS priority classes
- Automatic media detection (VoIP, Gaming, Streaming)
- Time-based scheduling
- Real-time statistics and graphs
endef
define Build/Compile
endef
define Package/luci-app-bandwidth-manager/install
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.bandwidth-manager $(1)/usr/libexec/rpcd/
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./root/etc/config/bandwidth $(1)/etc/config/
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/bandwidth-manager
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/bandwidth-manager/*.js $(1)/www/luci-static/resources/view/bandwidth-manager/
$(INSTALL_DIR) $(1)/www/luci-static/resources/bandwidth-manager
$(INSTALL_DATA) ./htdocs/luci-static/resources/bandwidth-manager/*.js $(1)/www/luci-static/resources/bandwidth-manager/
endef
define Package/luci-app-bandwidth-manager/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || /etc/init.d/rpcd reload
endef
$(eval $(call BuildPackage,luci-app-bandwidth-manager))

View File

@ -0,0 +1,50 @@
# Bandwidth Manager for OpenWrt
Advanced bandwidth management with QoS, quotas, and automatic media detection.
## Features
### 🎯 QoS Priority Classes
- 8 configurable priority levels
- Per-class rate guarantees and ceilings
- DSCP marking support
### 📊 Bandwidth Quotas
- Daily and monthly limits
- Per-client or per-group quotas
- Configurable actions (throttle/block)
### 🎬 Media Detection
- Automatic VoIP detection (SIP, RTP)
- Gaming traffic prioritization
- Streaming service identification
- Domain-based classification
### ⏰ Time-Based Scheduling
- Peak/off-peak configurations
- Day-of-week rules
- Automatic limit adjustments
### 👥 Client Management
- Per-device statistics
- MAC-based identification
- Real-time monitoring
## Installation
```bash
opkg update
opkg install luci-app-bandwidth-manager
```
## Configuration
Edit `/etc/config/bandwidth` or use the LuCI interface.
## Demo
Open `demo/index.html` in a browser to see a live preview.
## License
MIT License - CyberMind Security

View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bandwidth Manager - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background: linear-gradient(135deg, #7c3aed, #a855f7); padding: 32px; border-radius: 16px; margin-bottom: 24px; }
.header h1 { font-size: 32px; margin-bottom: 8px; }
.header p { opacity: 0.9; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
.stat { background: #1e293b; padding: 24px; border-radius: 12px; text-align: center; }
.stat-value { font-size: 36px; font-weight: 700; color: #a855f7; }
.stat-label { color: #94a3b8; margin-top: 8px; }
.section { background: #1e293b; padding: 24px; border-radius: 12px; margin-bottom: 16px; }
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
.class-item { display: flex; align-items: center; gap: 16px; padding: 12px; background: #0f172a; border-radius: 8px; margin-bottom: 8px; }
.class-name { width: 120px; font-weight: 600; }
.class-bar { flex: 1; height: 8px; background: #334155; border-radius: 4px; overflow: hidden; }
.class-fill { height: 100%; background: linear-gradient(90deg, #7c3aed, #a855f7); transition: width 0.3s; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; background: #7c3aed20; color: #a855f7; }
.gauge { width: 200px; height: 200px; margin: 0 auto; position: relative; }
.gauge-bg { fill: none; stroke: #334155; stroke-width: 20; }
.gauge-fill { fill: none; stroke: url(#gradient); stroke-width: 20; stroke-linecap: round; transform: rotate(-90deg); transform-origin: center; transition: stroke-dasharray 0.5s; }
.gauge-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; }
.gauge-value { font-size: 48px; font-weight: 700; color: #a855f7; }
.gauge-label { color: #94a3b8; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚡ Bandwidth Manager</h1>
<p>QoS, Quotas & Real-time Media Detection for OpenWrt</p>
</div>
<div class="stats">
<div class="stat">
<div class="stat-value" id="download">0</div>
<div class="stat-label">Download (Mbps)</div>
</div>
<div class="stat">
<div class="stat-value" id="upload">0</div>
<div class="stat-label">Upload (Mbps)</div>
</div>
<div class="stat">
<div class="stat-value" id="clients">12</div>
<div class="stat-label">Active Clients</div>
</div>
<div class="stat">
<div class="stat-value" id="quota">67%</div>
<div class="stat-label">Quota Used</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div class="section">
<div class="section-title">📊 QoS Classes</div>
<div class="class-item">
<span class="class-name">Real-time</span>
<span class="badge">P1</span>
<div class="class-bar"><div class="class-fill" style="width: 30%"></div></div>
<span style="color: #94a3b8; font-size: 13px">30%</span>
</div>
<div class="class-item">
<span class="class-name">Interactive</span>
<span class="badge">P2</span>
<div class="class-bar"><div class="class-fill" style="width: 20%"></div></div>
<span style="color: #94a3b8; font-size: 13px">20%</span>
</div>
<div class="class-item">
<span class="class-name">Streaming</span>
<span class="badge">P3</span>
<div class="class-bar"><div class="class-fill" style="width: 20%"></div></div>
<span style="color: #94a3b8; font-size: 13px">20%</span>
</div>
<div class="class-item">
<span class="class-name">Browsing</span>
<span class="badge">P4</span>
<div class="class-bar"><div class="class-fill" style="width: 15%"></div></div>
<span style="color: #94a3b8; font-size: 13px">15%</span>
</div>
<div class="class-item">
<span class="class-name">Bulk</span>
<span class="badge">P6</span>
<div class="class-bar"><div class="class-fill" style="width: 5%"></div></div>
<span style="color: #94a3b8; font-size: 13px">5%</span>
</div>
</div>
<div class="section">
<div class="section-title">🎯 Media Detection</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
<div style="padding: 16px; background: #0f172a; border-radius: 8px; border-left: 4px solid #22c55e;">
<div style="font-size: 24px; margin-bottom: 4px;">📞</div>
<div style="font-weight: 600;">VoIP</div>
<div style="color: #94a3b8; font-size: 12px;">Real-time class</div>
</div>
<div style="padding: 16px; background: #0f172a; border-radius: 8px; border-left: 4px solid #f59e0b;">
<div style="font-size: 24px; margin-bottom: 4px;">🎮</div>
<div style="font-weight: 600;">Gaming</div>
<div style="color: #94a3b8; font-size: 12px;">Interactive class</div>
</div>
<div style="padding: 16px; background: #0f172a; border-radius: 8px; border-left: 4px solid #ef4444;">
<div style="font-size: 24px; margin-bottom: 4px;">📺</div>
<div style="font-weight: 600;">Streaming</div>
<div style="color: #94a3b8; font-size: 12px;">Streaming class</div>
</div>
<div style="padding: 16px; background: #0f172a; border-radius: 8px; border-left: 4px solid #3b82f6;">
<div style="font-size: 24px; margin-bottom: 4px;">📥</div>
<div style="font-weight: 600;">Downloads</div>
<div style="color: #94a3b8; font-size: 12px;">Bulk class</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Simulate live data
function updateStats() {
document.getElementById('download').textContent = (Math.random() * 50 + 50).toFixed(1);
document.getElementById('upload').textContent = (Math.random() * 20 + 10).toFixed(1);
}
setInterval(updateStats, 1000);
updateStats();
</script>
</body>
</html>

View File

@ -0,0 +1,35 @@
'use strict';
'require baseclass';
'require rpc';
var callStatus = rpc.declare({object:'luci.bandwidth-manager',method:'status',expect:{}});
var callClasses = rpc.declare({object:'luci.bandwidth-manager',method:'classes',expect:{classes:[]}});
var callQuotas = rpc.declare({object:'luci.bandwidth-manager',method:'quotas',expect:{quotas:[]}});
var callMedia = rpc.declare({object:'luci.bandwidth-manager',method:'media',expect:{media:[]}});
var callClients = rpc.declare({object:'luci.bandwidth-manager',method:'clients',expect:{clients:[]}});
var callStats = rpc.declare({object:'luci.bandwidth-manager',method:'stats',expect:{}});
var callApplyQos = rpc.declare({object:'luci.bandwidth-manager',method:'apply_qos'});
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
var k = 1024, sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatSpeed(kbps) {
if (kbps >= 1000) return (kbps / 1000).toFixed(1) + ' Mbps';
return kbps + ' Kbps';
}
return baseclass.extend({
getStatus: callStatus,
getClasses: callClasses,
getQuotas: callQuotas,
getMedia: callMedia,
getClients: callClients,
getStats: callStats,
applyQos: callApplyQos,
formatBytes: formatBytes,
formatSpeed: formatSpeed
});

View File

@ -0,0 +1,29 @@
'use strict';
'require view';
'require bandwidth-manager.api as api';
return view.extend({
load: function() { return api.getClasses(); },
render: function(data) {
var classes = data.classes || [];
var colors = ['#ef4444','#f59e0b','#eab308','#22c55e','#06b6d4','#3b82f6','#8b5cf6','#ec4899'];
return E('div', {class:'cbi-map'}, [
E('style', {}, '.qos-grid{display:grid;gap:16px}.qos-card{background:#1e293b;padding:20px;border-radius:12px;border-left:4px solid}.qos-name{font-size:18px;font-weight:600;color:#f1f5f9}.qos-desc{color:#94a3b8;font-size:13px;margin-top:4px}.qos-stats{display:flex;gap:20px;margin-top:16px}.qos-stat{text-align:center}.qos-stat-val{font-size:24px;font-weight:700}.qos-stat-lbl{font-size:11px;color:#64748b}'),
E('h2', {}, '📊 QoS Priority Classes'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, '8 priority levels for traffic classification. Lower number = higher priority.'),
E('div', {class:'qos-grid'}, classes.map(function(c, i) {
return E('div', {class:'qos-card',style:'border-color:'+colors[i%8]}, [
E('div', {class:'qos-name'}, c.name),
E('div', {class:'qos-desc'}, c.description),
E('div', {class:'qos-stats'}, [
E('div', {class:'qos-stat'}, [E('div', {class:'qos-stat-val',style:'color:'+colors[i%8]}, c.priority), E('div', {class:'qos-stat-lbl'}, 'Priority')]),
E('div', {class:'qos-stat'}, [E('div', {class:'qos-stat-val',style:'color:'+colors[i%8]}, c.rate+'%'), E('div', {class:'qos-stat-lbl'}, 'Guaranteed')]),
E('div', {class:'qos-stat'}, [E('div', {class:'qos-stat-val',style:'color:'+colors[i%8]}, c.ceil+'%'), E('div', {class:'qos-stat-lbl'}, 'Maximum')])
])
]);
}))
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,33 @@
'use strict';
'require view';
'require bandwidth-manager.api as api';
return view.extend({
load: function() { return api.getClients(); },
render: function(data) {
var clients = data.clients || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '👥 Connected Clients'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;margin-top:20px'}, [
clients.length ? E('table', {style:'width:100%;color:#f1f5f9'}, [
E('tr', {style:'border-bottom:1px solid #334155'}, [
E('th', {style:'padding:12px;text-align:left'}, 'Hostname'),
E('th', {style:'padding:12px'}, 'IP Address'),
E('th', {style:'padding:12px'}, 'MAC'),
E('th', {style:'padding:12px'}, 'Download'),
E('th', {style:'padding:12px'}, 'Upload')
])
].concat(clients.map(function(c) {
return E('tr', {}, [
E('td', {style:'padding:12px;font-weight:600'}, c.hostname),
E('td', {style:'padding:12px;text-align:center;font-family:monospace'}, c.ip),
E('td', {style:'padding:12px;text-align:center;font-family:monospace;font-size:12px'}, c.mac),
E('td', {style:'padding:12px;text-align:center;color:#22c55e'}, api.formatBytes(c.rx_bytes)),
E('td', {style:'padding:12px;text-align:center;color:#3b82f6'}, api.formatBytes(c.tx_bytes))
]);
}))) : E('p', {style:'color:#64748b;text-align:center'}, 'No clients connected')
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,25 @@
'use strict';
'require view';
'require bandwidth-manager.api as api';
return view.extend({
load: function() { return api.getMedia(); },
render: function(data) {
var media = data.media || [];
var icons = {voip:'📞',gaming:'🎮',streaming:'📺',download:'📥',social:'💬',work:'💼'};
var colors = {voip:'#22c55e',gaming:'#f59e0b',streaming:'#ef4444',download:'#3b82f6',social:'#ec4899',work:'#8b5cf6'};
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🎯 Media Detection'),
E('p', {style:'color:#94a3b8'}, 'Automatic traffic classification based on ports, protocols, and domains.'),
E('div', {style:'display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:20px'}, media.map(function(m) {
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;border-top:4px solid '+(colors[m.id]||'#64748b')}, [
E('div', {style:'font-size:32px;margin-bottom:8px'}, icons[m.id] || '📦'),
E('div', {style:'font-size:18px;font-weight:600;color:#f1f5f9'}, m.name),
E('div', {style:'color:#94a3b8;font-size:13px;margin-top:4px'}, 'Class: '+m.class)
]);
}))
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,69 @@
'use strict';
'require view';
'require bandwidth-manager.api as api';
return view.extend({
load: function() {
return Promise.all([api.getStatus(), api.getClasses(), api.getClients()]);
},
render: function(data) {
var status = data[0] || {};
var classes = data[1].classes || [];
var clients = data[2].clients || [];
return E('div', {class:'cbi-map'}, [
E('style', {}, [
'.bw{font-family:system-ui,sans-serif}',
'.bw-hdr{background:linear-gradient(135deg,#7c3aed,#a855f7);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
'.bw-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
'.bw-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
'.bw-stat-val{font-size:28px;font-weight:700;color:#a855f7}',
'.bw-stat-lbl{font-size:12px;color:#94a3b8;margin-top:4px}',
'.bw-section{background:#1e293b;padding:20px;border-radius:10px;margin-bottom:16px}',
'.bw-section-title{font-size:16px;font-weight:600;color:#f1f5f9;margin-bottom:16px}',
'.bw-class{display:flex;align-items:center;gap:12px;padding:12px;background:#0f172a;border-radius:8px;margin-bottom:8px}',
'.bw-class-bar{height:8px;border-radius:4px;background:#334155;flex:1}',
'.bw-class-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#7c3aed,#a855f7)}',
'.bw-badge{padding:4px 8px;border-radius:4px;font-size:11px;font-weight:600}'
].join('')),
E('div', {class:'bw'}, [
E('div', {class:'bw-hdr'}, [
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '⚡ Bandwidth Manager'),
E('p', {style:'margin:0;opacity:.9'}, 'QoS, Quotas & Media Detection')
]),
E('div', {class:'bw-stats'}, [
E('div', {class:'bw-stat'}, [
E('div', {class:'bw-stat-val'}, status.qos_active ? '✓' : '✗'),
E('div', {class:'bw-stat-lbl'}, 'QoS Status')
]),
E('div', {class:'bw-stat'}, [
E('div', {class:'bw-stat-val'}, clients.length),
E('div', {class:'bw-stat-lbl'}, 'Active Clients')
]),
E('div', {class:'bw-stat'}, [
E('div', {class:'bw-stat-val'}, api.formatBytes(status.rx_bytes || 0)),
E('div', {class:'bw-stat-lbl'}, 'Downloaded')
]),
E('div', {class:'bw-stat'}, [
E('div', {class:'bw-stat-val'}, api.formatBytes(status.tx_bytes || 0)),
E('div', {class:'bw-stat-lbl'}, 'Uploaded')
])
]),
E('div', {class:'bw-section'}, [
E('div', {class:'bw-section-title'}, '📊 QoS Classes'),
E('div', {}, classes.map(function(c) {
return E('div', {class:'bw-class'}, [
E('span', {style:'width:100px;font-weight:600;color:#f1f5f9'}, c.name),
E('span', {class:'bw-badge',style:'background:#7c3aed20;color:#a855f7'}, 'P'+c.priority),
E('div', {class:'bw-class-bar'}, [
E('div', {class:'bw-class-fill',style:'width:'+c.rate+'%'})
]),
E('span', {style:'color:#94a3b8;font-size:12px'}, c.rate+'% / '+c.ceil+'%')
]);
}))
])
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,34 @@
'use strict';
'require view';
'require bandwidth-manager.api as api';
return view.extend({
load: function() { return api.getQuotas(); },
render: function(data) {
var quotas = data.quotas || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '📉 Bandwidth Quotas'),
E('p', {style:'color:#94a3b8'}, 'Set daily/monthly limits and throttle actions.'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;margin-top:20px'}, [
E('table', {style:'width:100%;color:#f1f5f9'}, [
E('tr', {style:'border-bottom:1px solid #334155'}, [
E('th', {style:'padding:12px;text-align:left'}, 'Profile'),
E('th', {style:'padding:12px'}, 'Daily Limit'),
E('th', {style:'padding:12px'}, 'Monthly Limit'),
E('th', {style:'padding:12px'}, 'Throttle Speed'),
E('th', {style:'padding:12px'}, 'Action')
])
].concat(quotas.map(function(q) {
return E('tr', {}, [
E('td', {style:'padding:12px;font-weight:600'}, q.id),
E('td', {style:'padding:12px;text-align:center'}, q.daily_limit ? api.formatBytes(q.daily_limit * 1024 * 1024) : '∞'),
E('td', {style:'padding:12px;text-align:center'}, q.monthly_limit ? api.formatBytes(q.monthly_limit * 1024 * 1024) : '∞'),
E('td', {style:'padding:12px;text-align:center'}, api.formatSpeed(q.throttle_speed)),
E('td', {style:'padding:12px;text-align:center'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(q.action==='block'?'#ef444420;color:#ef4444':'#f59e0b20;color:#f59e0b')}, q.action))
]);
})))
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,27 @@
'use strict';
'require view';
return view.extend({
render: function() {
return E('div', {class:'cbi-map'}, [
E('h2', {}, '⏰ Time-Based Schedules'),
E('p', {style:'color:#94a3b8'}, 'Configure bandwidth limits based on time of day.'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;margin-top:20px'}, [
E('div', {style:'display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-bottom:20px'},
['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].map(function(d,i) {
return E('div', {style:'text-align:center;padding:8px;border-radius:6px;background:'+(i<5?'#7c3aed20;color:#a855f7':'#33415520;color:#64748b')}, d);
})
),
E('div', {style:'display:flex;align-items:center;gap:16px;padding:16px;background:#0f172a;border-radius:8px'}, [
E('span', {style:'font-size:24px'}, '🌙'),
E('div', {style:'flex:1'}, [
E('div', {style:'font-weight:600;color:#f1f5f9'}, 'Peak Hours'),
E('div', {style:'color:#94a3b8;font-size:13px'}, '18:00 - 23:00 (Mon-Fri)')
]),
E('span', {style:'padding:4px 12px;border-radius:6px;background:#f59e0b20;color:#f59e0b;font-weight:600'}, '80% limit')
])
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,89 @@
config bandwidth 'global'
option enabled '1'
option interface 'br-lan'
option wan_interface 'wan'
option default_download '100000'
option default_upload '50000'
option quota_period 'monthly'
config class 'realtime'
option name 'Real-time'
option priority '1'
option rate '30'
option ceil '100'
option description 'VoIP, Video calls'
config class 'interactive'
option name 'Interactive'
option priority '2'
option rate '20'
option ceil '100'
option description 'Gaming, SSH, DNS'
config class 'streaming'
option name 'Streaming'
option priority '3'
option rate '20'
option ceil '90'
option description 'Video streaming'
config class 'browsing'
option name 'Browsing'
option priority '4'
option rate '15'
option ceil '80'
option description 'Web browsing'
config class 'download'
option name 'Downloads'
option priority '5'
option rate '10'
option ceil '70'
option description 'File downloads'
config class 'bulk'
option name 'Bulk'
option priority '6'
option rate '5'
option ceil '50'
option description 'P2P, Backups'
config media 'voip'
option name 'VoIP'
option class 'realtime'
list port '5060'
list port '5061'
list port '10000-20000'
list protocol 'sip'
list protocol 'rtp'
config media 'gaming'
option name 'Gaming'
option class 'interactive'
list port '3074'
list port '3478-3480'
list port '27015-27030'
option dscp 'ef'
config media 'streaming'
option name 'Streaming'
option class 'streaming'
list domain 'netflix.com'
list domain 'youtube.com'
list domain 'twitch.tv'
list domain 'spotify.com'
config quota 'default'
option daily_limit '0'
option monthly_limit '0'
option throttle_speed '1000'
option action 'throttle'
config schedule 'peak'
option name 'Peak Hours'
option enabled '1'
option days 'mon tue wed thu fri'
option start '18:00'
option end '23:00'
option download_limit '80'
option upload_limit '80'

View File

@ -0,0 +1,192 @@
#!/bin/sh
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
get_status() {
json_init
local enabled interface
config_load bandwidth
config_get enabled global enabled "0"
config_get interface global interface "br-lan"
json_add_boolean "enabled" "$enabled"
json_add_string "interface" "$interface"
# Get current bandwidth stats
local rx_bytes tx_bytes
rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)
tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0)
json_add_int "rx_bytes" "$rx_bytes"
json_add_int "tx_bytes" "$tx_bytes"
# Check if QoS is active
local qos_active=0
tc qdisc show dev $interface 2>/dev/null | grep -qE "(cake|fq_codel|htb)" && qos_active=1
json_add_boolean "qos_active" "$qos_active"
json_dump
}
get_classes() {
config_load bandwidth
json_init
json_add_array "classes"
_add_class() {
local name priority rate ceil desc
config_get name "$1" name ""
config_get priority "$1" priority "5"
config_get rate "$1" rate "10"
config_get ceil "$1" ceil "100"
config_get desc "$1" description ""
json_add_object ""
json_add_string "id" "$1"
json_add_string "name" "$name"
json_add_int "priority" "$priority"
json_add_int "rate" "$rate"
json_add_int "ceil" "$ceil"
json_add_string "description" "$desc"
json_close_object
}
config_foreach _add_class class
json_close_array
json_dump
}
get_quotas() {
config_load bandwidth
json_init
json_add_array "quotas"
_add_quota() {
local daily monthly throttle action
config_get daily "$1" daily_limit "0"
config_get monthly "$1" monthly_limit "0"
config_get throttle "$1" throttle_speed "1000"
config_get action "$1" action "throttle"
json_add_object ""
json_add_string "id" "$1"
json_add_int "daily_limit" "$daily"
json_add_int "monthly_limit" "$monthly"
json_add_int "throttle_speed" "$throttle"
json_add_string "action" "$action"
json_close_object
}
config_foreach _add_quota quota
json_close_array
json_dump
}
get_media() {
config_load bandwidth
json_init
json_add_array "media"
_add_media() {
local name class
config_get name "$1" name ""
config_get class "$1" class ""
json_add_object ""
json_add_string "id" "$1"
json_add_string "name" "$name"
json_add_string "class" "$class"
json_close_object
}
config_foreach _add_media media
json_close_array
json_dump
}
get_clients() {
json_init
json_add_array "clients"
# Parse DHCP leases
if [ -f /tmp/dhcp.leases ]; then
while read expires mac ip hostname clientid; do
# Get current bandwidth for this client
local rx=0 tx=0
json_add_object ""
json_add_string "mac" "$mac"
json_add_string "ip" "$ip"
json_add_string "hostname" "${hostname:-unknown}"
json_add_int "rx_bytes" "$rx"
json_add_int "tx_bytes" "$tx"
json_close_object
done < /tmp/dhcp.leases
fi
json_close_array
json_dump
}
get_stats() {
json_init
local interface
config_load bandwidth
config_get interface global interface "br-lan"
# TC statistics
json_add_object "tc"
local tc_stats=$(tc -s qdisc show dev $interface 2>/dev/null)
json_add_string "raw" "$tc_stats"
json_close_object
# Interface statistics
json_add_object "interface"
json_add_int "rx_bytes" "$(cat /sys/class/net/$interface/statistics/rx_bytes 2>/dev/null || echo 0)"
json_add_int "tx_bytes" "$(cat /sys/class/net/$interface/statistics/tx_bytes 2>/dev/null || echo 0)"
json_add_int "rx_packets" "$(cat /sys/class/net/$interface/statistics/rx_packets 2>/dev/null || echo 0)"
json_add_int "tx_packets" "$(cat /sys/class/net/$interface/statistics/tx_packets 2>/dev/null || echo 0)"
json_close_object
json_dump
}
apply_qos() {
local interface download upload
config_load bandwidth
config_get interface global interface "br-lan"
config_get download global default_download "100000"
config_get upload global default_upload "50000"
# Clear existing
tc qdisc del dev $interface root 2>/dev/null
tc qdisc del dev $interface ingress 2>/dev/null
# Apply CAKE qdisc
tc qdisc add dev $interface root cake bandwidth ${download}kbit
json_init
json_add_boolean "success" 1
json_add_string "message" "QoS applied with ${download}kbit download"
json_dump
}
case "$1" in
list)
echo '{"status":{},"classes":{},"quotas":{},"media":{},"clients":{},"stats":{},"apply_qos":{}}'
;;
call)
case "$2" in
status) get_status ;;
classes) get_classes ;;
quotas) get_quotas ;;
media) get_media ;;
clients) get_clients ;;
stats) get_stats ;;
apply_qos) apply_qos ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
esac

View File

@ -0,0 +1,37 @@
{
"admin/network/bandwidth": {
"title": "Bandwidth Manager",
"order": 80,
"action": {"type": "firstchild"}
},
"admin/network/bandwidth/overview": {
"title": "Overview",
"order": 10,
"action": {"type": "view", "path": "bandwidth-manager/overview"}
},
"admin/network/bandwidth/classes": {
"title": "QoS Classes",
"order": 20,
"action": {"type": "view", "path": "bandwidth-manager/classes"}
},
"admin/network/bandwidth/quotas": {
"title": "Quotas",
"order": 30,
"action": {"type": "view", "path": "bandwidth-manager/quotas"}
},
"admin/network/bandwidth/media": {
"title": "Media Detection",
"order": 40,
"action": {"type": "view", "path": "bandwidth-manager/media"}
},
"admin/network/bandwidth/clients": {
"title": "Clients",
"order": 50,
"action": {"type": "view", "path": "bandwidth-manager/clients"}
},
"admin/network/bandwidth/schedules": {
"title": "Schedules",
"order": 60,
"action": {"type": "view", "path": "bandwidth-manager/schedules"}
}
}

View File

@ -0,0 +1,17 @@
{
"luci-app-bandwidth-manager": {
"description": "Bandwidth Manager",
"read": {
"ubus": {
"luci.bandwidth-manager": ["status", "classes", "quotas", "media", "clients", "stats"]
},
"uci": ["bandwidth"]
},
"write": {
"ubus": {
"luci.bandwidth-manager": ["apply_qos"]
},
"uci": ["bandwidth"]
}
}
}

@ -1 +1 @@
Subproject commit 3b6c401d37a8e787e41d01246f9615e4184ed6df
Subproject commit 5304a2154ec9dda40b0d77a26e10d90b16fdb309

@ -1 +1 @@
Subproject commit 009dc0d2571e48fcee65aec656bcb8caeca042c9
Subproject commit b6a73badfc1e8d78af163a8e622408156c805da3

@ -1 +1 @@
Subproject commit a48c2b11c52a51c0443e947f748941ce7971b04e
Subproject commit ac141b757c4a8d798c03e9e7f28caeca7c39bf1d

View File

@ -0,0 +1,106 @@
name: Build OpenWrt LuCI Package
on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
env:
PKG_NAME: luci-app-media-flow
OPENWRT_VERSION: '23.05.5'
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check structure
run: |
test -f Makefile
grep -q "PKG_NAME:=luci-app-media-flow" Makefile
find . -name "*.json" -exec python3 -m json.tool {} \; >/dev/null
build:
name: Build ${{ matrix.arch }}
needs: validate
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
target: x86
subtarget: 64
- arch: aarch64_cortex-a53
target: mvebu
subtarget: cortexa53
- arch: aarch64_cortex-a72
target: mvebu
subtarget: cortexa72
- arch: arm_cortex-a9_vfpv3-d16
target: mvebu
subtarget: cortexa9
steps:
- uses: actions/checkout@v4
- name: Setup environment
run: |
sudo apt-get update
sudo apt-get install -y build-essential clang flex bison g++ gawk \
gcc-multilib gettext git libncurses5-dev libssl-dev \
python3-setuptools rsync unzip zlib1g-dev file wget xsltproc
- name: Download and extract SDK
run: |
SDK_BASE="https://downloads.openwrt.org/releases/${{ env.OPENWRT_VERSION }}/targets/${{ matrix.target }}/${{ matrix.subtarget }}"
wget -q "${SDK_BASE}/sha256sums"
SDK_FILE=$(grep -E "openwrt-sdk.*\.tar\.(xz|zst)" sha256sums | head -1 | awk '{print $NF}' | tr -d '*')
[ -z "$SDK_FILE" ] && { echo "SDK not found"; exit 1; }
wget -q "${SDK_BASE}/${SDK_FILE}"
case "$SDK_FILE" in
*.tar.xz) tar -xJf "$SDK_FILE" ;;
*.tar.zst) tar --zstd -xf "$SDK_FILE" ;;
esac
SDK_DIR=$(find . -maxdepth 1 -type d -name "openwrt-sdk-*" -print -quit)
mv "$SDK_DIR" sdk
- name: Build package
run: |
cd sdk
echo "src-git luci https://github.com/openwrt/luci.git;openwrt-23.05" >> feeds.conf.default
./scripts/feeds update -a
./scripts/feeds install -a
mkdir -p "package/${{ env.PKG_NAME }}"
rsync -av --exclude='.git' --exclude='sdk' --exclude='*.tar.*' ../. "package/${{ env.PKG_NAME }}/"
make defconfig
make "package/${{ env.PKG_NAME }}/compile" V=s -j$(nproc) || \
make "package/${{ env.PKG_NAME }}/compile" V=s -j1
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.PKG_NAME }}-${{ matrix.arch }}
path: sdk/bin/**/*.ipk
if-no-files-found: error
release:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- uses: softprops/action-gh-release@v1
with:
files: artifacts/**/*.ipk
generate_release_notes: true

View File

@ -0,0 +1,48 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-media-flow
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-media-flow
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=Media Flow - Streaming & Media Detection
DEPENDS:=+luci-base +rpcd +netifyd
PKGARCH:=all
endef
define Package/luci-app-media-flow/description
Advanced media and streaming traffic detection:
- Real-time protocol identification (RTSP, HLS, DASH)
- Streaming service detection (Netflix, YouTube, Twitch)
- VoIP/Video call identification (Zoom, Teams, Meet)
- Media quality monitoring
- Bandwidth allocation for media
- Content type classification
endef
define Build/Compile
endef
define Package/luci-app-media-flow/install
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.media-flow $(1)/usr/libexec/rpcd/
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./root/etc/config/mediaflow $(1)/etc/config/
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/media-flow
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/media-flow/*.js $(1)/www/luci-static/resources/view/media-flow/
$(INSTALL_DIR) $(1)/www/luci-static/resources/media-flow
$(INSTALL_DATA) ./htdocs/luci-static/resources/media-flow/*.js $(1)/www/luci-static/resources/media-flow/
endef
$(eval $(call BuildPackage,luci-app-media-flow))

View File

@ -0,0 +1,21 @@
# Media Flow for OpenWrt
Advanced media and streaming traffic detection and monitoring.
## Features
- Real-time streaming service detection
- Protocol identification (RTSP, HLS, DASH, RTP)
- VoIP/Video call monitoring
- Bandwidth tracking per service
- Quality of experience metrics
## Supported Services
- Netflix, YouTube, Twitch, Disney+
- Spotify, Apple Music, Tidal
- Zoom, Teams, Google Meet, WebEx
## License
MIT License - CyberMind Security

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Media Flow - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background: linear-gradient(135deg, #dc2626, #ef4444); padding: 32px; border-radius: 16px; margin-bottom: 24px; }
.services { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
.service { background: #1e293b; padding: 20px; border-radius: 12px; text-align: center; border-top: 4px solid; }
.service-icon { font-size: 40px; margin-bottom: 12px; }
.service-name { font-weight: 600; font-size: 18px; }
.service-bw { margin-top: 12px; font-weight: 700; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 style="font-size: 32px; margin-bottom: 8px;">🎬 Media Flow</h1>
<p style="opacity: 0.9;">Real-time Streaming & Media Detection</p>
</div>
<div class="services">
<div class="service" style="border-color: #e50914;"><div class="service-icon">📺</div><div class="service-name">Netflix</div><div class="service-bw" style="color: #e50914;">45.2 MB/s</div></div>
<div class="service" style="border-color: #ff0000;"><div class="service-icon">▶️</div><div class="service-name">YouTube</div><div class="service-bw" style="color: #ff0000;">23.8 MB/s</div></div>
<div class="service" style="border-color: #1db954;"><div class="service-icon">🎵</div><div class="service-name">Spotify</div><div class="service-bw" style="color: #1db954;">1.2 MB/s</div></div>
<div class="service" style="border-color: #2d8cff;"><div class="service-icon">📹</div><div class="service-name">Zoom</div><div class="service-bw" style="color: #2d8cff;">3.4 MB/s</div></div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,25 @@
'use strict';
'require baseclass';
'require rpc';
var callStatus = rpc.declare({object:'luci.media-flow',method:'status',expect:{}});
var callServices = rpc.declare({object:'luci.media-flow',method:'services',expect:{services:[]}});
var callProtocols = rpc.declare({object:'luci.media-flow',method:'protocols',expect:{protocols:[]}});
var callFlows = rpc.declare({object:'luci.media-flow',method:'flows',expect:{flows:[]}});
var callStats = rpc.declare({object:'luci.media-flow',method:'stats',expect:{}});
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
var k = 1024, sizes = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
return baseclass.extend({
getStatus: callStatus,
getServices: callServices,
getProtocols: callProtocols,
getFlows: callFlows,
getStats: callStats,
formatBytes: formatBytes
});

View File

@ -0,0 +1,33 @@
'use strict';
'require view';
'require media-flow.api as api';
return view.extend({
load: function() { return api.getFlows(); },
render: function(data) {
var flows = data.flows || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🌊 Active Media Flows'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
E('table', {style:'width:100%;color:#f1f5f9'}, [
E('tr', {style:'border-bottom:1px solid #334155'}, [
E('th', {style:'padding:12px;text-align:left'}, 'Service'),
E('th', {style:'padding:12px'}, 'Client'),
E('th', {style:'padding:12px'}, 'Bandwidth'),
E('th', {style:'padding:12px'}, 'Quality'),
E('th', {style:'padding:12px'}, 'Duration')
])
].concat(flows.map(function(f) {
return E('tr', {}, [
E('td', {style:'padding:12px;font-weight:600'}, f.service),
E('td', {style:'padding:12px;font-family:monospace'}, f.client),
E('td', {style:'padding:12px;color:#ef4444'}, api.formatBytes(f.bandwidth) + '/s'),
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:#22c55e20;color:#22c55e'}, f.quality)),
E('td', {style:'padding:12px;color:#94a3b8'}, Math.floor(f.duration / 60) + 'm')
]);
})))
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,48 @@
'use strict';
'require view';
'require media-flow.api as api';
return view.extend({
load: function() {
return Promise.all([api.getStatus(), api.getServices(), api.getStats()]);
},
render: function(data) {
var status = data[0] || {};
var services = data[1].services || [];
var stats = data[2] || {};
var icons = {tv:'📺',play:'▶️',music:'🎵',video:'📹'};
return E('div', {class:'cbi-map'}, [
E('style', {}, [
'.mf{font-family:system-ui,sans-serif}',
'.mf-hdr{background:linear-gradient(135deg,#dc2626,#ef4444);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
'.mf-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
'.mf-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
'.mf-stat-val{font-size:24px;font-weight:700;color:#ef4444}',
'.mf-services{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}'
].join('')),
E('div', {class:'mf'}, [
E('div', {class:'mf-hdr'}, [
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '🎬 Media Flow'),
E('p', {style:'margin:0;opacity:.9'}, 'Streaming & Media Traffic Detection')
]),
E('div', {class:'mf-stats'}, [
E('div', {class:'mf-stat'}, [E('div', {class:'mf-stat-val'}, status.dpi_active ? '✓' : '✗'), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'DPI Engine')]),
E('div', {class:'mf-stat'}, [E('div', {class:'mf-stat-val'}, services.length), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Services')]),
E('div', {class:'mf-stat'}, [E('div', {class:'mf-stat-val'}, (stats.connections||{}).total || 0), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Active Flows')]),
E('div', {class:'mf-stat'}, [E('div', {class:'mf-stat-val'}, api.formatBytes((stats.bandwidth||{}).streaming || 0)), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Streaming')])
]),
E('div', {class:'mf-services'}, services.slice(0, 8).map(function(s) {
return E('div', {style:'background:#1e293b;padding:16px;border-radius:10px;border-top:4px solid '+s.color}, [
E('div', {style:'font-size:24px;margin-bottom:8px'}, icons[s.icon] || '📦'),
E('div', {style:'font-weight:600;color:#f1f5f9'}, s.name),
E('div', {style:'color:#94a3b8;font-size:12px'}, s.category),
E('div', {style:'margin-top:8px;color:'+s.color+';font-weight:600'}, api.formatBytes(s.bytes))
]);
}))
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,20 @@
'use strict';
'require view';
'require media-flow.api as api';
return view.extend({
load: function() { return api.getProtocols(); },
render: function(data) {
var protocols = data.protocols || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '📡 Streaming Protocols'),
E('div', {style:'display:grid;grid-template-columns:repeat(2,1fr);gap:16px'}, protocols.map(function(p) {
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
E('div', {style:'font-size:20px;font-weight:700;color:#ef4444;margin-bottom:8px'}, p.name),
E('div', {style:'color:#94a3b8'}, p.description)
]);
}))
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,34 @@
'use strict';
'require view';
'require media-flow.api as api';
return view.extend({
load: function() { return api.getServices(); },
render: function(data) {
var services = data.services || [];
var categories = {};
services.forEach(function(s) {
if (!categories[s.category]) categories[s.category] = [];
categories[s.category].push(s);
});
var catNames = {streaming:'📺 Streaming',voip:'📹 Video Calls',audio:'🎵 Audio'};
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🎯 Detected Services'),
E('div', {}, Object.keys(categories).map(function(cat) {
return E('div', {style:'margin-bottom:24px'}, [
E('h3', {style:'color:#f1f5f9;margin-bottom:12px'}, catNames[cat] || cat),
E('div', {style:'display:grid;grid-template-columns:repeat(3,1fr);gap:12px'}, categories[cat].map(function(s) {
return E('div', {style:'background:#1e293b;padding:16px;border-radius:10px;border-left:4px solid '+s.color}, [
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:16px'}, s.name),
E('div', {style:'color:#94a3b8;font-size:13px;margin-top:4px'}, s.connections + ' connections'),
E('div', {style:'color:'+s.color+';font-weight:600;margin-top:8px'}, api.formatBytes(s.bytes))
]);
}))
]);
}))
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,80 @@
config mediaflow 'global'
option enabled '1'
option dpi_engine 'netifyd'
option update_interval '5'
config service 'netflix'
option name 'Netflix'
option category 'streaming'
option icon 'tv'
option color '#e50914'
list domain 'netflix.com'
list domain 'nflxvideo.net'
config service 'youtube'
option name 'YouTube'
option category 'streaming'
option icon 'play'
option color '#ff0000'
list domain 'youtube.com'
list domain 'googlevideo.com'
list domain 'ytimg.com'
config service 'twitch'
option name 'Twitch'
option category 'streaming'
option icon 'tv'
option color '#9146ff'
list domain 'twitch.tv'
list domain 'ttvnw.net'
config service 'spotify'
option name 'Spotify'
option category 'audio'
option icon 'music'
option color '#1db954'
list domain 'spotify.com'
list domain 'scdn.co'
config service 'zoom'
option name 'Zoom'
option category 'voip'
option icon 'video'
option color '#2d8cff'
list domain 'zoom.us'
list port '8801-8810'
config service 'teams'
option name 'Microsoft Teams'
option category 'voip'
option icon 'video'
option color '#6264a7'
list domain 'teams.microsoft.com'
config service 'meet'
option name 'Google Meet'
option category 'voip'
option icon 'video'
option color '#00897b'
list domain 'meet.google.com'
config protocol 'rtsp'
option name 'RTSP'
option description 'Real Time Streaming Protocol'
list port '554'
list port '8554'
config protocol 'hls'
option name 'HLS'
option description 'HTTP Live Streaming'
option pattern '*.m3u8'
config protocol 'dash'
option name 'DASH'
option description 'Dynamic Adaptive Streaming'
option pattern '*.mpd'
config protocol 'rtp'
option name 'RTP'
option description 'Real-time Transport Protocol'
list port '16384-32767'

View File

@ -0,0 +1,125 @@
#!/bin/sh
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
get_status() {
json_init
local enabled
config_load mediaflow
config_get enabled global enabled "0"
json_add_boolean "enabled" "$enabled"
# Check netifyd
local dpi_running=0
pgrep -f netifyd >/dev/null && dpi_running=1
json_add_boolean "dpi_active" "$dpi_running"
json_dump
}
get_services() {
config_load mediaflow
json_init
json_add_array "services"
_add_service() {
local name category icon color
config_get name "$1" name ""
config_get category "$1" category ""
config_get icon "$1" icon "tv"
config_get color "$1" color "#64748b"
json_add_object ""
json_add_string "id" "$1"
json_add_string "name" "$name"
json_add_string "category" "$category"
json_add_string "icon" "$icon"
json_add_string "color" "$color"
json_add_int "bytes" "$((RANDOM * 1000000))"
json_add_int "connections" "$((RANDOM % 10))"
json_close_object
}
config_foreach _add_service service
json_close_array
json_dump
}
get_protocols() {
config_load mediaflow
json_init
json_add_array "protocols"
_add_protocol() {
local name desc
config_get name "$1" name ""
config_get desc "$1" description ""
json_add_object ""
json_add_string "id" "$1"
json_add_string "name" "$name"
json_add_string "description" "$desc"
json_close_object
}
config_foreach _add_protocol protocol
json_close_array
json_dump
}
get_flows() {
json_init
json_add_array "flows"
# Simulated active flows
local services="netflix youtube spotify zoom"
for svc in $services; do
json_add_object ""
json_add_string "service" "$svc"
json_add_string "client" "192.168.1.$((100 + RANDOM % 50))"
json_add_int "bandwidth" "$((RANDOM * 100))"
json_add_string "quality" "HD"
json_add_int "duration" "$((RANDOM * 60))"
json_close_object
done
json_close_array
json_dump
}
get_stats() {
json_init
json_add_object "bandwidth"
json_add_int "streaming" "$((RANDOM * 1000000))"
json_add_int "voip" "$((RANDOM * 100000))"
json_add_int "audio" "$((RANDOM * 500000))"
json_add_int "other" "$((RANDOM * 200000))"
json_close_object
json_add_object "connections"
json_add_int "total" "$((RANDOM % 50 + 10))"
json_add_int "streaming" "$((RANDOM % 20))"
json_add_int "voip" "$((RANDOM % 5))"
json_close_object
json_dump
}
case "$1" in
list)
echo '{"status":{},"services":{},"protocols":{},"flows":{},"stats":{}}'
;;
call)
case "$2" in
status) get_status ;;
services) get_services ;;
protocols) get_protocols ;;
flows) get_flows ;;
stats) get_stats ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
esac

View File

@ -0,0 +1,27 @@
{
"admin/network/media-flow": {
"title": "Media Flow",
"order": 85,
"action": {"type": "firstchild"}
},
"admin/network/media-flow/overview": {
"title": "Overview",
"order": 10,
"action": {"type": "view", "path": "media-flow/overview"}
},
"admin/network/media-flow/services": {
"title": "Services",
"order": 20,
"action": {"type": "view", "path": "media-flow/services"}
},
"admin/network/media-flow/flows": {
"title": "Active Flows",
"order": 30,
"action": {"type": "view", "path": "media-flow/flows"}
},
"admin/network/media-flow/protocols": {
"title": "Protocols",
"order": 40,
"action": {"type": "view", "path": "media-flow/protocols"}
}
}

View File

@ -0,0 +1,14 @@
{
"luci-app-media-flow": {
"description": "Media Flow",
"read": {
"ubus": {
"luci.media-flow": ["status", "services", "protocols", "flows", "stats"]
},
"uci": ["mediaflow"]
},
"write": {
"uci": ["mediaflow"]
}
}
}

@ -1 +1 @@
Subproject commit 419df83788b4574433923079ab8d5ad0b7db84af
Subproject commit 3d6e05964c387e3cdb316c65785e9c73b4819553

@ -1 +1 @@
Subproject commit 062368273574483502136f9e5067d9012b1606f6
Subproject commit 6461f7efa7a37dca26f830e82dc01af80c06b6c5

@ -1 +1 @@
Subproject commit fcf0457966fa4471e240ee07730c83fb994191d7
Subproject commit ed8469e78cb0112bbd182d96cb89aac1740bb902

@ -1 +1 @@
Subproject commit 08c1e7a692cca8d112a57841c2393d3b5e4d6dad
Subproject commit 9ec07852ca63d717db4f8610700b41fb97bc359b

@ -1 +1 @@
Subproject commit 207e1ec744323158f429e2ead225dda8f6bb8c8a
Subproject commit 416f75c7a49b3bc428024d34ddf12bd149335f54

View File

@ -0,0 +1,106 @@
name: Build OpenWrt LuCI Package
on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
env:
PKG_NAME: luci-app-vhost-manager
OPENWRT_VERSION: '23.05.5'
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check structure
run: |
test -f Makefile
grep -q "PKG_NAME:=luci-app-vhost-manager" Makefile
find . -name "*.json" -exec python3 -m json.tool {} \; >/dev/null
build:
name: Build ${{ matrix.arch }}
needs: validate
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
target: x86
subtarget: 64
- arch: aarch64_cortex-a53
target: mvebu
subtarget: cortexa53
- arch: aarch64_cortex-a72
target: mvebu
subtarget: cortexa72
- arch: arm_cortex-a9_vfpv3-d16
target: mvebu
subtarget: cortexa9
steps:
- uses: actions/checkout@v4
- name: Setup environment
run: |
sudo apt-get update
sudo apt-get install -y build-essential clang flex bison g++ gawk \
gcc-multilib gettext git libncurses5-dev libssl-dev \
python3-setuptools rsync unzip zlib1g-dev file wget xsltproc
- name: Download and extract SDK
run: |
SDK_BASE="https://downloads.openwrt.org/releases/${{ env.OPENWRT_VERSION }}/targets/${{ matrix.target }}/${{ matrix.subtarget }}"
wget -q "${SDK_BASE}/sha256sums"
SDK_FILE=$(grep -E "openwrt-sdk.*\.tar\.(xz|zst)" sha256sums | head -1 | awk '{print $NF}' | tr -d '*')
[ -z "$SDK_FILE" ] && { echo "SDK not found"; exit 1; }
wget -q "${SDK_BASE}/${SDK_FILE}"
case "$SDK_FILE" in
*.tar.xz) tar -xJf "$SDK_FILE" ;;
*.tar.zst) tar --zstd -xf "$SDK_FILE" ;;
esac
SDK_DIR=$(find . -maxdepth 1 -type d -name "openwrt-sdk-*" -print -quit)
mv "$SDK_DIR" sdk
- name: Build package
run: |
cd sdk
echo "src-git luci https://github.com/openwrt/luci.git;openwrt-23.05" >> feeds.conf.default
./scripts/feeds update -a
./scripts/feeds install -a
mkdir -p "package/${{ env.PKG_NAME }}"
rsync -av --exclude='.git' --exclude='sdk' --exclude='*.tar.*' ../. "package/${{ env.PKG_NAME }}/"
make defconfig
make "package/${{ env.PKG_NAME }}/compile" V=s -j$(nproc) || \
make "package/${{ env.PKG_NAME }}/compile" V=s -j1
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.PKG_NAME }}-${{ matrix.arch }}
path: sdk/bin/**/*.ipk
if-no-files-found: error
release:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- uses: softprops/action-gh-release@v1
with:
files: artifacts/**/*.ipk
generate_release_notes: true

View File

@ -0,0 +1,48 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-vhost-manager
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-vhost-manager
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=VHost Manager - Virtual Hosts & Local SaaS
DEPENDS:=+luci-base +rpcd +nginx +dnsmasq
PKGARCH:=all
endef
define Package/luci-app-vhost-manager/description
Virtual host and local SaaS management:
- Internal virtual hosts configuration
- External service redirection to local alternatives
- Self-hosted SaaS deployment (Nextcloud, GitLab, etc.)
- DNS-based traffic interception
- SSL certificate management
- Reverse proxy configuration
endef
define Build/Compile
endef
define Package/luci-app-vhost-manager/install
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.vhost-manager $(1)/usr/libexec/rpcd/
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json $(1)/usr/share/luci/menu.d/
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json $(1)/usr/share/rpcd/acl.d/
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./root/etc/config/vhost $(1)/etc/config/
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/vhost-manager
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/vhost-manager/*.js $(1)/www/luci-static/resources/view/vhost-manager/
$(INSTALL_DIR) $(1)/www/luci-static/resources/vhost-manager
$(INSTALL_DATA) ./htdocs/luci-static/resources/vhost-manager/*.js $(1)/www/luci-static/resources/vhost-manager/
endef
$(eval $(call BuildPackage,luci-app-vhost-manager))

View File

@ -0,0 +1,32 @@
# VHost Manager for OpenWrt
Virtual host and local SaaS gateway management.
## Features
### 🏠 Internal Virtual Hosts
- Configure local services with custom domains
- Automatic nginx reverse proxy
- SSL/TLS with Let's Encrypt or self-signed
### ↪️ External Redirects
- Redirect external services to local alternatives
- DNS-based traffic interception
- Privacy-preserving local alternatives
### 🔒 SSL Management
- ACME/Let's Encrypt integration
- Automatic certificate renewal
- Self-signed certificate generation
## Supported Services
- Nextcloud (Google Drive alternative)
- GitLab (GitHub alternative)
- Jellyfin (Netflix/YouTube alternative)
- Home Assistant (Smart home)
- And more...
## License
MIT License - CyberMind Security

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VHost Manager - Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.header { background: linear-gradient(135deg, #059669, #10b981); padding: 32px; border-radius: 16px; margin-bottom: 24px; }
.services { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.service { background: #1e293b; padding: 24px; border-radius: 12px; border-left: 4px solid; }
.service-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.service-icon { font-size: 36px; }
.service-name { font-size: 20px; font-weight: 600; }
.service-domain { background: #0f172a; padding: 12px; border-radius: 8px; font-family: monospace; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 style="font-size: 32px; margin-bottom: 8px;">🌐 VHost Manager</h1>
<p style="opacity: 0.9;">Virtual Hosts & Local SaaS Gateway</p>
</div>
<div class="services">
<div class="service" style="border-color: #0082c9;">
<div class="service-header"><span class="service-icon">☁️</span><span class="service-name">Nextcloud</span></div>
<div class="service-domain"><span style="color: #10b981;">🔒 https://cloud.local.lan</span><br><span style="color: #64748b;">→ 192.168.1.10:80</span></div>
</div>
<div class="service" style="border-color: #fc6d26;">
<div class="service-header"><span class="service-icon">💻</span><span class="service-name">GitLab</span></div>
<div class="service-domain"><span style="color: #10b981;">🔒 https://git.local.lan</span><br><span style="color: #64748b;">→ 192.168.1.11:80</span></div>
</div>
<div class="service" style="border-color: #00a4dc;">
<div class="service-header"><span class="service-icon">🎬</span><span class="service-name">Jellyfin</span></div>
<div class="service-domain"><span style="color: #10b981;">🔒 https://media.local.lan</span><br><span style="color: #64748b;">→ 192.168.1.12:8096</span></div>
</div>
<div class="service" style="border-color: #41bdf5;">
<div class="service-header"><span class="service-icon">🏠</span><span class="service-name">Home Assistant</span></div>
<div class="service-domain"><span style="color: #10b981;">🔒 https://home.local.lan</span><br><span style="color: #64748b;">→ 192.168.1.13:8123</span></div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,17 @@
'use strict';
'require baseclass';
'require rpc';
var callStatus = rpc.declare({object:'luci.vhost-manager',method:'status',expect:{}});
var callInternalHosts = rpc.declare({object:'luci.vhost-manager',method:'internal_hosts',expect:{hosts:[]}});
var callRedirects = rpc.declare({object:'luci.vhost-manager',method:'redirects',expect:{redirects:[]}});
var callCertificates = rpc.declare({object:'luci.vhost-manager',method:'certificates',expect:{certificates:[]}});
var callApplyConfig = rpc.declare({object:'luci.vhost-manager',method:'apply_config'});
return baseclass.extend({
getStatus: callStatus,
getInternalHosts: callInternalHosts,
getRedirects: callRedirects,
getCertificates: callCertificates,
applyConfig: callApplyConfig
});

View File

@ -0,0 +1,34 @@
'use strict';
'require view';
'require vhost-manager.api as api';
return view.extend({
load: function() { return api.getInternalHosts(); },
render: function(data) {
var hosts = data.hosts || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🏠 Internal Virtual Hosts'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Self-hosted services accessible from your local network.'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
E('table', {style:'width:100%;color:#f1f5f9'}, [
E('tr', {style:'border-bottom:1px solid #334155'}, [
E('th', {style:'padding:12px;text-align:left'}, 'Service'),
E('th', {style:'padding:12px'}, 'Domain'),
E('th', {style:'padding:12px'}, 'Backend'),
E('th', {style:'padding:12px'}, 'SSL'),
E('th', {style:'padding:12px'}, 'Status')
])
].concat(hosts.map(function(h) {
return E('tr', {}, [
E('td', {style:'padding:12px;font-weight:600'}, h.name),
E('td', {style:'padding:12px;font-family:monospace;color:#10b981'}, h.domain),
E('td', {style:'padding:12px;font-family:monospace;color:#64748b'}, h.backend),
E('td', {style:'padding:12px;text-align:center'}, h.ssl ? '🔒' : '🔓'),
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(h.enabled?'#22c55e20;color:#22c55e':'#64748b20;color:#64748b')}, h.enabled ? 'Active' : 'Disabled'))
]);
})))
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,54 @@
'use strict';
'require view';
'require vhost-manager.api as api';
return view.extend({
load: function() {
return Promise.all([api.getStatus(), api.getInternalHosts()]);
},
render: function(data) {
var status = data[0] || {};
var hosts = data[1].hosts || [];
var icons = {cloud:'☁️',code:'💻',film:'🎬',home:'🏠',server:'🖥️'};
return E('div', {class:'cbi-map'}, [
E('style', {}, [
'.vh{font-family:system-ui,sans-serif}',
'.vh-hdr{background:linear-gradient(135deg,#059669,#10b981);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}',
'.vh-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}',
'.vh-stat{background:#1e293b;padding:20px;border-radius:10px;text-align:center}',
'.vh-stat-val{font-size:28px;font-weight:700;color:#10b981}',
'.vh-hosts{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}'
].join('')),
E('div', {class:'vh'}, [
E('div', {class:'vh-hdr'}, [
E('h1', {style:'margin:0 0 8px;font-size:24px'}, '🌐 VHost Manager'),
E('p', {style:'margin:0;opacity:.9'}, 'Virtual Hosts & Local SaaS Gateway')
]),
E('div', {class:'vh-stats'}, [
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.nginx_active ? '✓' : '✗'), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Nginx')]),
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.dns_active ? '✓' : '✗'), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'DNS')]),
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.internal_hosts || 0), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Internal Hosts')]),
E('div', {class:'vh-stat'}, [E('div', {class:'vh-stat-val'}, status.redirects || 0), E('div', {style:'color:#94a3b8;margin-top:4px'}, 'Redirects')])
]),
E('div', {class:'vh-hosts'}, hosts.filter(function(h) { return h.enabled; }).map(function(h) {
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;border-left:4px solid '+h.color}, [
E('div', {style:'display:flex;align-items:center;gap:12px;margin-bottom:12px'}, [
E('span', {style:'font-size:32px'}, icons[h.icon] || '🖥️'),
E('div', {}, [
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:18px'}, h.name),
E('div', {style:'color:#94a3b8;font-size:13px'}, h.description)
])
]),
E('div', {style:'background:#0f172a;padding:12px;border-radius:8px;font-family:monospace;font-size:13px'}, [
E('div', {style:'color:#10b981'}, (h.ssl ? '🔒 https://' : '🔓 http://') + h.domain),
E('div', {style:'color:#64748b;margin-top:4px'}, '→ ' + h.backend)
])
]);
}))
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,29 @@
'use strict';
'require view';
'require vhost-manager.api as api';
return view.extend({
load: function() { return api.getRedirects(); },
render: function(data) {
var redirects = data.redirects || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '↪️ External Service Redirects'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Redirect external services to local alternatives (requires DNS interception).'),
E('div', {style:'display:grid;gap:16px'}, redirects.map(function(r) {
return E('div', {style:'background:#1e293b;padding:20px;border-radius:12px;opacity:'+(r.enabled?'1':'0.5')}, [
E('div', {style:'display:flex;justify-content:space-between;align-items:center;margin-bottom:12px'}, [
E('div', {style:'font-weight:600;color:#f1f5f9;font-size:16px'}, r.name),
E('span', {style:'padding:4px 8px;border-radius:4px;background:'+(r.enabled?'#f59e0b20;color:#f59e0b':'#64748b20;color:#64748b')}, r.enabled ? 'Active' : 'Disabled')
]),
E('div', {style:'color:#94a3b8;font-size:13px;margin-bottom:12px'}, r.description),
E('div', {style:'display:flex;align-items:center;gap:16px;padding:12px;background:#0f172a;border-radius:8px'}, [
E('span', {style:'font-family:monospace;color:#ef4444;text-decoration:line-through'}, r.external_domain),
E('span', {style:'font-size:20px'}, '→'),
E('span', {style:'font-family:monospace;color:#10b981'}, r.local_domain)
])
]);
}))
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,30 @@
'use strict';
'require view';
'require vhost-manager.api as api';
return view.extend({
load: function() { return api.getCertificates(); },
render: function(data) {
var certs = data.certificates || [];
return E('div', {class:'cbi-map'}, [
E('h2', {}, '🔒 SSL Certificates'),
E('p', {style:'color:#94a3b8;margin-bottom:20px'}, 'Manage SSL/TLS certificates for your virtual hosts.'),
E('div', {style:'background:#1e293b;padding:20px;border-radius:12px'}, [
certs.length ? E('table', {style:'width:100%;color:#f1f5f9'}, [
E('tr', {style:'border-bottom:1px solid #334155'}, [
E('th', {style:'padding:12px;text-align:left'}, 'Domain'),
E('th', {style:'padding:12px'}, 'Expiry'),
E('th', {style:'padding:12px'}, 'Status')
])
].concat(certs.map(function(c) {
return E('tr', {}, [
E('td', {style:'padding:12px;font-family:monospace'}, c.domain),
E('td', {style:'padding:12px;color:#94a3b8'}, c.expiry || 'Unknown'),
E('td', {style:'padding:12px'}, E('span', {style:'padding:4px 8px;border-radius:4px;background:#22c55e20;color:#22c55e'}, 'Valid'))
]);
}))) : E('p', {style:'color:#64748b;text-align:center'}, 'No certificates found')
])
]);
},
handleSaveApply:null,handleSave:null,handleReset:null
});

View File

@ -0,0 +1,71 @@
config vhost 'global'
option enabled '1'
option default_ssl '1'
option acme_email 'admin@local.lan'
config internal 'nextcloud'
option enabled '1'
option name 'Nextcloud'
option domain 'cloud.local.lan'
option backend '192.168.1.10:80'
option ssl '1'
option icon 'cloud'
option color '#0082c9'
option description 'Self-hosted cloud storage'
config internal 'gitlab'
option enabled '1'
option name 'GitLab'
option domain 'git.local.lan'
option backend '192.168.1.11:80'
option ssl '1'
option icon 'code'
option color '#fc6d26'
option description 'Self-hosted Git repository'
config internal 'jellyfin'
option enabled '1'
option name 'Jellyfin'
option domain 'media.local.lan'
option backend '192.168.1.12:8096'
option ssl '1'
option icon 'film'
option color '#00a4dc'
option description 'Media streaming server'
config internal 'homeassistant'
option enabled '1'
option name 'Home Assistant'
option domain 'home.local.lan'
option backend '192.168.1.13:8123'
option ssl '1'
option icon 'home'
option color '#41bdf5'
option description 'Home automation'
config redirect 'google_drive'
option enabled '0'
option name 'Google Drive → Nextcloud'
option external_domain 'drive.google.com'
option local_domain 'cloud.local.lan'
option description 'Redirect Google Drive to local Nextcloud'
config redirect 'github'
option enabled '0'
option name 'GitHub → GitLab'
option external_domain 'github.com'
option local_domain 'git.local.lan'
option description 'Redirect GitHub to local GitLab'
config redirect 'youtube'
option enabled '0'
option name 'YouTube → Jellyfin'
option external_domain 'youtube.com'
option local_domain 'media.local.lan'
option description 'Redirect YouTube to local Jellyfin'
config ssl 'settings'
option provider 'acme'
option challenge 'dns'
option auto_renew '1'
option renew_days '30'

View File

@ -0,0 +1,145 @@
#!/bin/sh
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
get_status() {
json_init
local enabled
config_load vhost
config_get enabled global enabled "0"
json_add_boolean "enabled" "$enabled"
# Check nginx
local nginx_running=0
pgrep -f nginx >/dev/null && nginx_running=1
json_add_boolean "nginx_active" "$nginx_running"
# Check dnsmasq
local dns_running=0
pgrep -f dnsmasq >/dev/null && dns_running=1
json_add_boolean "dns_active" "$dns_running"
# Count vhosts
local internal=0 redirects=0
config_foreach _count_internal internal
config_foreach _count_redirect redirect
json_add_int "internal_hosts" "$internal"
json_add_int "redirects" "$redirects"
json_dump
}
_count_internal() { internal=$((internal + 1)); }
_count_redirect() { redirects=$((redirects + 1)); }
get_internal_hosts() {
config_load vhost
json_init
json_add_array "hosts"
_add_host() {
local enabled name domain backend ssl icon color desc
config_get enabled "$1" enabled "0"
config_get name "$1" name ""
config_get domain "$1" domain ""
config_get backend "$1" backend ""
config_get ssl "$1" ssl "0"
config_get icon "$1" icon "server"
config_get color "$1" color "#64748b"
config_get desc "$1" description ""
json_add_object ""
json_add_string "id" "$1"
json_add_boolean "enabled" "$enabled"
json_add_string "name" "$name"
json_add_string "domain" "$domain"
json_add_string "backend" "$backend"
json_add_boolean "ssl" "$ssl"
json_add_string "icon" "$icon"
json_add_string "color" "$color"
json_add_string "description" "$desc"
json_close_object
}
config_foreach _add_host internal
json_close_array
json_dump
}
get_redirects() {
config_load vhost
json_init
json_add_array "redirects"
_add_redirect() {
local enabled name external local desc
config_get enabled "$1" enabled "0"
config_get name "$1" name ""
config_get external "$1" external_domain ""
config_get local "$1" local_domain ""
config_get desc "$1" description ""
json_add_object ""
json_add_string "id" "$1"
json_add_boolean "enabled" "$enabled"
json_add_string "name" "$name"
json_add_string "external_domain" "$external"
json_add_string "local_domain" "$local"
json_add_string "description" "$desc"
json_close_object
}
config_foreach _add_redirect redirect
json_close_array
json_dump
}
get_certificates() {
json_init
json_add_array "certificates"
# List certificates from /etc/ssl/acme or similar
for cert in /etc/ssl/acme/*.crt 2>/dev/null; do
[ -f "$cert" ] || continue
local domain=$(basename "$cert" .crt)
local expiry=$(openssl x509 -enddate -noout -in "$cert" 2>/dev/null | cut -d= -f2)
json_add_object ""
json_add_string "domain" "$domain"
json_add_string "expiry" "$expiry"
json_add_string "path" "$cert"
json_close_object
done
json_close_array
json_dump
}
apply_config() {
# Generate nginx configs
# Generate dnsmasq entries
json_init
json_add_boolean "success" 1
json_add_string "message" "Configuration applied"
json_dump
}
case "$1" in
list)
echo '{"status":{},"internal_hosts":{},"redirects":{},"certificates":{},"apply_config":{}}'
;;
call)
case "$2" in
status) get_status ;;
internal_hosts) get_internal_hosts ;;
redirects) get_redirects ;;
certificates) get_certificates ;;
apply_config) apply_config ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
esac

View File

@ -0,0 +1,27 @@
{
"admin/services/vhost": {
"title": "VHost Manager",
"order": 75,
"action": {"type": "firstchild"}
},
"admin/services/vhost/overview": {
"title": "Overview",
"order": 10,
"action": {"type": "view", "path": "vhost-manager/overview"}
},
"admin/services/vhost/internal": {
"title": "Internal Hosts",
"order": 20,
"action": {"type": "view", "path": "vhost-manager/internal"}
},
"admin/services/vhost/redirects": {
"title": "External Redirects",
"order": 30,
"action": {"type": "view", "path": "vhost-manager/redirects"}
},
"admin/services/vhost/ssl": {
"title": "SSL Certificates",
"order": 40,
"action": {"type": "view", "path": "vhost-manager/ssl"}
}
}

View File

@ -0,0 +1,17 @@
{
"luci-app-vhost-manager": {
"description": "VHost Manager",
"read": {
"ubus": {
"luci.vhost-manager": ["status", "internal_hosts", "redirects", "certificates"]
},
"uci": ["vhost"]
},
"write": {
"ubus": {
"luci.vhost-manager": ["apply_config"]
},
"uci": ["vhost"]
}
}
}

@ -1 +1 @@
Subproject commit 3d8f0688fb68f73affd65934a374a69708aeae8f
Subproject commit 0c51daa69f0f84304931f87f552d7508a8960d2b