feat(service-registry): Add public IP detection and external port check

- Add get_network_info RPCD method:
  - Public IPv4/IPv6 detection via external services
  - Reverse DNS hostname lookup
  - External port accessibility test (upstream router/ISP check)
- Enhance check_service_health:
  - Compare DNS resolution against actual public IP
  - Detect private IP misconfiguration (192.168.x.x pointing)
  - Test external port reachability
- Add Network Connectivity panel to dashboard:
  - Shows public IPs with hostnames
  - External port 80/443 accessibility status
  - Local firewall and HAProxy status
- Improve URL Readiness Checker:
  - Display public IP info
  - Show specific recommendations with IP addresses
  - Detect and explain DNS pointing to private IP

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-28 07:15:49 +01:00
parent 99612f0c1a
commit e01504d4a8
5 changed files with 525 additions and 38 deletions

View File

@ -17,6 +17,15 @@ Unified service aggregation dashboard with automatic publishing to HAProxy (clea
## Dashboard
### Network Connectivity Panel
Real-time network status showing:
- **Public IPv4** - Your external IP address with reverse DNS hostname
- **Public IPv6** - IPv6 address if available
- **External Port 80/443** - Whether ports are reachable from the internet (tests upstream router/ISP forwarding)
- **Local Firewall** - OpenWrt firewall rule status
- **HAProxy** - Reverse proxy container status
### Health Summary Bar
Shows overall system status at a glance:
@ -32,12 +41,17 @@ Before publishing a service, verify the domain is properly configured:
1. Enter a domain in the checker (e.g., `example.com`)
2. Click "Check" to verify:
- **DNS Resolution** - Domain resolves to expected IP
- **Firewall Ports** - Ports 80 and 443 open from WAN
- **Your Public IP** - Shows your IPv4/IPv6 addresses and reverse DNS
- **DNS Resolution** - Verifies domain resolves to your public IP (detects private IP misconfiguration)
- **Internet Accessibility** - Tests if ports 80/443 are reachable from internet (upstream router check)
- **Local Firewall** - OpenWrt firewall rule status
- **SSL Certificate** - Valid certificate with expiry status
- **HAProxy** - Reverse proxy container running
The checker provides actionable recommendations when issues are found.
The checker provides specific actionable recommendations:
- If DNS points to private IP (e.g., 192.168.x.x), shows the correct public IP to use
- If ports are blocked externally, advises checking upstream router port forwarding
- Shows exact DNS A record to create: `domain.com → your.public.ip`
### Service Health Indicators
@ -96,6 +110,39 @@ When you publish a service with a domain:
## Health Check API
### Get Network Info
```bash
ubus call luci.service-registry get_network_info
```
Response:
```json
{
"success": true,
"lan_ip": "192.168.255.1",
"ipv4": {
"address": "185.220.101.12",
"status": "ok",
"hostname": "server.example.com"
},
"ipv6": {
"address": "2001:db8::1",
"status": "ok"
},
"external_ports": {
"http": { "accessible": true, "status": "open" },
"https": { "accessible": true, "status": "open" }
},
"firewall": {
"status": "ok",
"http_open": true,
"https_open": true
},
"haproxy": { "status": "running" }
}
```
### Check Single Domain
```bash
@ -107,25 +154,41 @@ Response:
{
"success": true,
"domain": "example.com",
"public_ip": {
"ipv4": "185.220.101.12",
"ipv6": "2001:db8::1",
"hostname": "server.example.com"
},
"dns": {
"status": "ok",
"resolved_ip": "203.0.113.10"
"resolved_ip": "185.220.101.12"
},
"certificate": {
"external_access": {
"status": "ok",
"days_left": 45
"http_accessible": true,
"https_accessible": true
},
"firewall": {
"status": "ok",
"http_open": true,
"https_open": true
},
"certificate": {
"status": "ok",
"days_left": 45
},
"haproxy": {
"status": "running"
}
}
```
DNS status values:
- `ok` - Domain resolves to your public IP
- `private` - Domain resolves to a private IP (192.168.x.x, 10.x.x.x, etc.)
- `mismatch` - Domain resolves to a different public IP
- `failed` - DNS resolution failed
### Check All Services
```bash
@ -222,8 +285,9 @@ uci commit service-registry
| `list_services` | List all services from all providers |
| `publish_service` | Publish a service to HAProxy/Tor |
| `unpublish_service` | Remove service from HAProxy/Tor |
| `check_service_health` | Check DNS/cert/firewall for domain |
| `check_service_health` | Check DNS/cert/firewall/external access for domain |
| `check_all_health` | Batch health check all services |
| `get_network_info` | Get public IPs, external port accessibility, firewall status |
| `generate_landing_page` | Regenerate static landing page |
## License

View File

@ -100,6 +100,12 @@ var callCheckAllHealth = rpc.declare({
expect: {}
});
var callGetNetworkInfo = rpc.declare({
object: 'luci.service-registry',
method: 'get_network_info',
expect: {}
});
// HAProxy status for provider info
var callHAProxyStatus = rpc.declare({
object: 'luci.haproxy',
@ -245,6 +251,11 @@ return baseclass.extend({
return callCheckAllHealth();
},
// Get network connectivity info (public IPs, port accessibility)
getNetworkInfo: function() {
return callGetNetworkInfo();
},
// Get dashboard data with health status
getDashboardDataWithHealth: function() {
return Promise.all([

View File

@ -50,9 +50,15 @@ return view.extend({
var published = services.filter(function(s) { return s.published; });
var unpublished = services.filter(function(s) { return !s.published; });
// Load network info asynchronously
var networkPanel = E('div', { 'id': 'sr-network-panel', 'class': 'sr-network-loading' },
E('span', { 'class': 'spinning' }, 'Loading network info...'));
this.loadNetworkInfo(networkPanel);
return E('div', { 'class': 'sr-compact' }, [
this.renderHeader(services, providers, data.haproxy, data.tor),
this.renderHealthSummary(data.health),
networkPanel,
this.renderUrlChecker(),
this.renderSection('📡 Published Services', published, true),
this.renderSection('🔍 Discovered Services', unpublished, false),
@ -60,6 +66,108 @@ return view.extend({
]);
},
loadNetworkInfo: function(container) {
api.getNetworkInfo().then(function(data) {
if (!data.success) {
container.innerHTML = '<div class="sr-network-error">Failed to load network info</div>';
return;
}
var ipv4 = data.ipv4 || {};
var ipv6 = data.ipv6 || {};
var extPorts = data.external_ports || {};
var firewall = data.firewall || {};
var html = '<div class="sr-network-card">';
html += '<div class="sr-network-header">🌍 Network Connectivity</div>';
html += '<div class="sr-network-grid">';
// IPv4
html += '<div class="sr-network-item">';
html += '<span class="sr-network-label">IPv4</span>';
if (ipv4.address) {
html += '<span class="sr-network-value sr-network-ok">' + ipv4.address + '</span>';
if (ipv4.hostname) {
html += '<span class="sr-network-sub">' + ipv4.hostname + '</span>';
}
} else {
html += '<span class="sr-network-value sr-network-na">Not available</span>';
}
html += '</div>';
// IPv6
html += '<div class="sr-network-item">';
html += '<span class="sr-network-label">IPv6</span>';
if (ipv6.address) {
html += '<span class="sr-network-value sr-network-ok" style="font-size:0.75em;">' + ipv6.address + '</span>';
if (ipv6.hostname) {
html += '<span class="sr-network-sub">' + ipv6.hostname + '</span>';
}
} else {
html += '<span class="sr-network-value sr-network-na">Not available</span>';
}
html += '</div>';
// External Port 80
html += '<div class="sr-network-item">';
html += '<span class="sr-network-label">Port 80 (HTTP)</span>';
var http = extPorts.http || {};
if (http.status === 'open') {
html += '<span class="sr-network-value sr-network-ok">✅ Open from Internet</span>';
} else if (http.status === 'blocked') {
html += '<span class="sr-network-value sr-network-fail">🚫 Blocked</span>';
html += '<span class="sr-network-sub">' + (http.hint || 'Check router') + '</span>';
} else {
html += '<span class="sr-network-value sr-network-na">Unknown</span>';
}
html += '</div>';
// External Port 443
html += '<div class="sr-network-item">';
html += '<span class="sr-network-label">Port 443 (HTTPS)</span>';
var https = extPorts.https || {};
if (https.status === 'open') {
html += '<span class="sr-network-value sr-network-ok">✅ Open from Internet</span>';
} else if (https.status === 'blocked') {
html += '<span class="sr-network-value sr-network-fail">🚫 Blocked</span>';
html += '<span class="sr-network-sub">' + (https.hint || 'Check router') + '</span>';
} else {
html += '<span class="sr-network-value sr-network-na">Unknown</span>';
}
html += '</div>';
// Local Firewall
html += '<div class="sr-network-item">';
html += '<span class="sr-network-label">Local Firewall</span>';
if (firewall.status === 'ok') {
html += '<span class="sr-network-value sr-network-ok">✅ Ports 80/443 open</span>';
} else if (firewall.status === 'partial') {
html += '<span class="sr-network-value sr-network-warn">⚠️ Partial</span>';
} else {
html += '<span class="sr-network-value sr-network-fail">🚫 Closed</span>';
}
html += '</div>';
// HAProxy
html += '<div class="sr-network-item">';
html += '<span class="sr-network-label">HAProxy</span>';
var haproxy = data.haproxy || {};
if (haproxy.status === 'running') {
html += '<span class="sr-network-value sr-network-ok">🟢 Running</span>';
} else {
html += '<span class="sr-network-value sr-network-fail">🔴 Stopped</span>';
}
html += '</div>';
html += '</div></div>';
container.className = 'sr-network-loaded';
container.innerHTML = html;
}).catch(function(err) {
container.innerHTML = '<div class="sr-network-error">Error: ' + err.message + '</div>';
});
},
renderHealthSummary: function(health) {
if (!health || !health.firewall) return E('div');
@ -136,46 +244,90 @@ return view.extend({
var html = '<div class="sr-check-grid">';
// DNS Status
// Public IP Info
var publicIp = result.public_ip || {};
html += '<div class="sr-check-item sr-check-info">';
html += '<span class="sr-check-icon">🌍</span>';
html += '<span class="sr-check-label">Your Public IP</span>';
html += '<span class="sr-check-value">';
if (publicIp.ipv4) {
html += 'IPv4: <strong>' + publicIp.ipv4 + '</strong>';
if (publicIp.hostname) html += ' (' + publicIp.hostname + ')';
}
if (publicIp.ipv6) {
html += '<br>IPv6: <strong style="font-size:0.8em;">' + publicIp.ipv6 + '</strong>';
}
html += '</span></div>';
// DNS Status with IP comparison
var dnsStatus = result.dns || {};
var dnsIcon = healthIcons.dns[dnsStatus.status] || '❓';
var dnsClass = dnsStatus.status === 'ok' ? 'sr-check-ok' : 'sr-check-fail';
var dnsClass = 'sr-check-fail';
if (dnsStatus.status === 'ok') dnsClass = 'sr-check-ok';
else if (dnsStatus.status === 'private' || dnsStatus.status === 'mismatch') dnsClass = 'sr-check-warn';
html += '<div class="sr-check-item ' + dnsClass + '">';
html += '<span class="sr-check-icon">' + dnsIcon + '</span>';
html += '<span class="sr-check-icon">🌐</span>';
html += '<span class="sr-check-label">DNS Resolution</span>';
if (dnsStatus.status === 'ok') {
html += '<span class="sr-check-value">✅ Resolves to ' + dnsStatus.resolved_ip + '</span>';
html += '<span class="sr-check-value">✅ Resolves to ' + dnsStatus.resolved_ip + ' (matches public IP)</span>';
} else if (dnsStatus.status === 'private') {
html += '<span class="sr-check-value">⚠️ Resolves to <strong>' + dnsStatus.resolved_ip + '</strong> (private IP!)</span>';
html += '<span class="sr-check-sub">Should be: ' + dnsStatus.expected + '</span>';
} else if (dnsStatus.status === 'mismatch') {
html += '<span class="sr-check-value">⚠️ Resolves to ' + dnsStatus.resolved_ip + '</span>';
html += '<span class="sr-check-sub">Your public IP: ' + dnsStatus.expected + '</span>';
} else {
html += '<span class="sr-check-value">❌ DNS not configured or not resolving</span>';
}
html += '</div>';
// Firewall Status
// External Port Accessibility
var extAccess = result.external_access || {};
var extClass = extAccess.status === 'ok' ? 'sr-check-ok' : (extAccess.status === 'partial' ? 'sr-check-warn' : 'sr-check-fail');
html += '<div class="sr-check-item ' + extClass + '">';
html += '<span class="sr-check-icon">🔌</span>';
html += '<span class="sr-check-label">Internet Accessibility</span>';
if (extAccess.status === 'ok') {
html += '<span class="sr-check-value">✅ Ports 80 & 443 reachable from internet</span>';
} else if (extAccess.status === 'partial') {
var open = [];
var closed = [];
if (extAccess.http_accessible) open.push('80'); else closed.push('80');
if (extAccess.https_accessible) open.push('443'); else closed.push('443');
html += '<span class="sr-check-value">⚠️ Open: ' + open.join(',') + ' | Blocked: ' + closed.join(',') + '</span>';
html += '<span class="sr-check-sub">' + (extAccess.hint || '') + '</span>';
} else if (extAccess.status === 'blocked') {
html += '<span class="sr-check-value">🚫 Ports NOT reachable from internet</span>';
html += '<span class="sr-check-sub">' + (extAccess.hint || 'Check upstream router/ISP port forwarding') + '</span>';
} else {
html += '<span class="sr-check-value">❓ Could not test external accessibility</span>';
}
html += '</div>';
// Local Firewall Status
var fwStatus = result.firewall || {};
var fwIcon = healthIcons.firewall[fwStatus.status] || '❓';
var fwClass = fwStatus.status === 'ok' ? 'sr-check-ok' : (fwStatus.status === 'partial' ? 'sr-check-warn' : 'sr-check-fail');
html += '<div class="sr-check-item ' + fwClass + '">';
html += '<span class="sr-check-icon">' + fwIcon + '</span>';
html += '<span class="sr-check-label">Firewall Ports</span>';
html += '<span class="sr-check-icon">🛡️</span>';
html += '<span class="sr-check-label">Local Firewall</span>';
var ports = [];
if (fwStatus.http_open) ports.push('80');
if (fwStatus.https_open) ports.push('443');
html += '<span class="sr-check-value">' + (ports.length ? 'Open: ' + ports.join(', ') : '❌ Ports 80/443 not open') + '</span>';
html += '<span class="sr-check-value">' + (ports.length === 2 ? '✅ Ports 80/443 open' : (ports.length ? '⚠️ Only port ' + ports.join(',') + ' open' : '❌ Ports closed')) + '</span>';
html += '</div>';
// Certificate Status
var certStatus = result.certificate || {};
var certIcon = healthIcons.cert[certStatus.status] || '❓';
var certClass = certStatus.status === 'ok' ? 'sr-check-ok' : (certStatus.status === 'warning' ? 'sr-check-warn' : 'sr-check-fail');
html += '<div class="sr-check-item ' + certClass + '">';
html += '<span class="sr-check-icon">' + certIcon + '</span>';
html += '<span class="sr-check-icon">🔒</span>';
html += '<span class="sr-check-label">SSL Certificate</span>';
if (certStatus.status === 'ok' || certStatus.status === 'warning') {
html += '<span class="sr-check-value">' + certStatus.days_left + ' days remaining</span>';
html += '<span class="sr-check-value">' + (certStatus.status === 'ok' ? '✅' : '⚠️') + ' ' + certStatus.days_left + ' days remaining</span>';
} else if (certStatus.status === 'expired') {
html += '<span class="sr-check-value">❌ Certificate expired</span>';
} else if (certStatus.status === 'missing') {
html += '<span class="sr-check-value">⚪ No certificate (request via HAProxy)</span>';
html += '<span class="sr-check-value">⚪ No certificate yet</span>';
} else {
html += '<span class="sr-check-value">⚪ Not applicable</span>';
}
@ -183,10 +335,9 @@ return view.extend({
// HAProxy Status
var haStatus = result.haproxy || {};
var haIcon = haStatus.status === 'running' ? '🟢' : '🔴';
var haClass = haStatus.status === 'running' ? 'sr-check-ok' : 'sr-check-fail';
html += '<div class="sr-check-item ' + haClass + '">';
html += '<span class="sr-check-icon">' + haIcon + '</span>';
html += '<span class="sr-check-icon">' + (haStatus.status === 'running' ? '🟢' : '🔴') + '</span>';
html += '<span class="sr-check-label">HAProxy</span>';
html += '<span class="sr-check-value">' + (haStatus.status === 'running' ? '✅ Running' : '❌ Not running') + '</span>';
html += '</div>';
@ -194,24 +345,36 @@ return view.extend({
html += '</div>';
// Summary and recommendation
var allOk = dnsStatus.status === 'ok' && fwStatus.status === 'ok' && haStatus.status === 'running';
var dnsOk = dnsStatus.status === 'ok';
var extOk = extAccess.status === 'ok';
var fwOk = fwStatus.status === 'ok';
var haOk = haStatus.status === 'running';
var certOk = certStatus.status === 'ok' || certStatus.status === 'warning';
var needsCert = certStatus.status === 'missing';
var allOk = dnsOk && extOk && fwOk && haOk;
html += '<div class="sr-check-summary">';
if (allOk && !needsCert) {
html += '<div class="sr-check-ready">✅ ' + domain + ' is ready and serving!</div>';
if (allOk && certOk) {
html += '<div class="sr-check-ready">✅ ' + domain + ' is fully operational!</div>';
} else if (allOk && needsCert) {
html += '<div class="sr-check-almost">⚠️ ' + domain + ' is ready - just need SSL certificate</div>';
html += '<a href="/cgi-bin/luci/admin/services/haproxy/certificates" class="sr-check-action">📜 Request Certificate</a>';
} else {
html += '<div class="sr-check-notready">❌ ' + domain + ' needs configuration</div>';
if (dnsStatus.status !== 'ok') {
html += '<div class="sr-check-tip">💡 Point DNS A record to your public IP</div>';
if (dnsStatus.status === 'private') {
html += '<div class="sr-check-tip">💡 <strong>DNS points to private IP!</strong> Update A record to: <code>' + publicIp.ipv4 + '</code></div>';
} else if (dnsStatus.status === 'mismatch') {
html += '<div class="sr-check-tip">💡 DNS points to different IP. Update A record to: <code>' + publicIp.ipv4 + '</code></div>';
} else if (dnsStatus.status !== 'ok') {
html += '<div class="sr-check-tip">💡 Create DNS A record: ' + domain + ' → ' + publicIp.ipv4 + '</div>';
}
if (fwStatus.status !== 'ok') {
html += '<div class="sr-check-tip">💡 Open ports 80 and 443 in firewall</div>';
if (!extOk && extAccess.status !== 'unknown') {
html += '<div class="sr-check-tip">💡 <strong>Port forwarding needed!</strong> Forward ports 80/443 on your router to this device</div>';
}
if (haStatus.status !== 'running') {
if (!fwOk) {
html += '<div class="sr-check-tip">💡 Open ports 80 and 443 in local firewall</div>';
}
if (!haOk) {
html += '<div class="sr-check-tip">💡 Start HAProxy container</div>';
}
}
@ -550,6 +713,24 @@ return view.extend({
@media (prefers-color-scheme: dark) { .sr-health-bar { background: #1a2a3e; } }
.sr-health-item { font-size: 0.9em; }
/* Network Info Panel */
.sr-network-loading { padding: 20px; text-align: center; background: #f8f8f8; border-radius: 8px; margin-bottom: 15px; }
@media (prefers-color-scheme: dark) { .sr-network-loading { background: #1a1a2e; } }
.sr-network-loaded { margin-bottom: 15px; }
.sr-network-error { padding: 15px; background: #fef2f2; color: #dc2626; border-radius: 8px; margin-bottom: 15px; }
@media (prefers-color-scheme: dark) { .sr-network-error { background: #450a0a; color: #fca5a5; } }
.sr-network-card { background: linear-gradient(135deg, #1e3a5f 0%, #0d2137 100%); border-radius: 12px; padding: 20px; color: #fff; }
.sr-network-header { font-size: 1.1em; font-weight: 600; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.1); }
.sr-network-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; }
.sr-network-item { background: rgba(0,0,0,0.2); padding: 12px; border-radius: 8px; }
.sr-network-label { display: block; font-size: 0.8em; color: #94a3b8; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px; }
.sr-network-value { display: block; font-size: 0.95em; font-weight: 500; word-break: break-all; }
.sr-network-value.sr-network-ok { color: #22c55e; }
.sr-network-value.sr-network-fail { color: #ef4444; }
.sr-network-value.sr-network-warn { color: #eab308; }
.sr-network-value.sr-network-na { color: #64748b; font-style: italic; }
.sr-network-sub { display: block; font-size: 0.75em; color: #64748b; margin-top: 4px; }
/* URL Checker Wizard Card */
.sr-wizard-card { background: linear-gradient(135deg, #0a192f 0%, #172a45 100%); border-radius: 12px; padding: 20px; margin-bottom: 25px; color: #fff; }
.sr-wizard-header { display: flex; align-items: center; gap: 12px; margin-bottom: 15px; }
@ -569,14 +750,20 @@ return view.extend({
.sr-check-item.sr-check-ok { border-left-color: #22c55e; }
.sr-check-item.sr-check-warn { border-left-color: #eab308; }
.sr-check-item.sr-check-fail { border-left-color: #ef4444; }
.sr-check-icon { font-size: 1.3em; }
.sr-check-label { font-weight: 600; font-size: 0.9em; min-width: 100px; }
.sr-check-value { font-size: 0.85em; opacity: 0.8; }
.sr-check-item.sr-check-info { border-left-color: #0099cc; }
.sr-check-icon { font-size: 1.3em; flex-shrink: 0; }
.sr-check-label { font-weight: 600; font-size: 0.9em; min-width: 100px; flex-shrink: 0; }
.sr-check-value { font-size: 0.85em; opacity: 0.9; flex: 1; }
.sr-check-value code { background: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 3px; font-family: monospace; }
.sr-check-value strong { color: #fff; }
.sr-check-sub { display: block; font-size: 0.8em; color: #94a3b8; margin-top: 4px; }
.sr-check-summary { margin-top: 15px; padding: 15px; background: #0f172a; border-radius: 8px; text-align: center; }
.sr-check-ready { font-size: 1.1em; color: #22c55e; font-weight: 600; }
.sr-check-almost { font-size: 1.1em; color: #eab308; font-weight: 600; }
.sr-check-notready { font-size: 1.1em; color: #ef4444; font-weight: 600; margin-bottom: 10px; }
.sr-check-tip { font-size: 0.85em; opacity: 0.8; margin-top: 5px; }
.sr-check-tip { font-size: 0.85em; opacity: 0.9; margin-top: 8px; text-align: left; padding: 0 20px; }
.sr-check-tip code { background: rgba(0,0,0,0.3); padding: 2px 8px; border-radius: 3px; font-family: monospace; color: #0ff; }
.sr-check-tip strong { color: #fbbf24; }
.sr-check-action { display: inline-block; margin-top: 10px; padding: 8px 16px; background: #0099cc; color: #fff; text-decoration: none; border-radius: 6px; font-size: 0.9em; }
.sr-check-action:hover { background: #00b3e6; }

View File

@ -901,7 +901,7 @@ check_dns_resolution() {
return 1
}
# Helper: Get WAN IP address
# Helper: Get WAN IP address (local interface)
get_wan_ip() {
local wan_ip=""
# Try to get WAN IP from interface
@ -917,6 +917,62 @@ get_wan_ip() {
echo "$wan_ip"
}
# Helper: Get public IPv4 address (from external service)
get_public_ipv4() {
local ip=""
# Try multiple services for reliability
ip=$(wget -qO- -T 5 "http://ipv4.icanhazip.com" 2>/dev/null | tr -d '\n')
[ -z "$ip" ] && ip=$(wget -qO- -T 5 "http://api.ipify.org" 2>/dev/null | tr -d '\n')
[ -z "$ip" ] && ip=$(wget -qO- -T 5 "http://v4.ident.me" 2>/dev/null | tr -d '\n')
echo "$ip"
}
# Helper: Get public IPv6 address (from external service)
get_public_ipv6() {
local ip=""
# Try multiple services for reliability
ip=$(wget -qO- -T 5 "http://ipv6.icanhazip.com" 2>/dev/null | tr -d '\n')
[ -z "$ip" ] && ip=$(wget -qO- -T 5 "http://v6.ident.me" 2>/dev/null | tr -d '\n')
echo "$ip"
}
# Helper: Check external port accessibility using portchecker service
check_external_port() {
local ip="$1"
local port="$2"
local result=""
# Use canyouseeme.org API or similar
# Try portquiz.net which echoes back on any port
result=$(wget -qO- -T 5 "http://portquiz.net:${port}/" 2>/dev/null)
if echo "$result" | grep -q "Port ${port}"; then
return 0
fi
# Alternative: try to connect to our own IP from outside perspective
# Use online port checker API
result=$(wget -qO- -T 8 "https://ports.yougetsignal.com/short-url-check-port.php?remoteAddress=${ip}&portNumber=${port}" 2>/dev/null)
if echo "$result" | grep -qi "open"; then
return 0
fi
return 1
}
# Helper: Reverse DNS lookup
get_reverse_dns() {
local ip="$1"
local hostname=""
if command -v nslookup >/dev/null 2>&1; then
hostname=$(nslookup "$ip" 2>/dev/null | grep "name =" | head -1 | awk '{print $NF}' | sed 's/\.$//')
elif command -v host >/dev/null 2>&1; then
hostname=$(host "$ip" 2>/dev/null | grep "pointer" | head -1 | awk '{print $NF}' | sed 's/\.$//')
fi
echo "$hostname"
}
# Helper: Check certificate expiry
check_cert_expiry() {
local domain="$1"
@ -975,6 +1031,116 @@ check_port_firewall_open() {
return 1
}
# Get network connectivity info (public IPs, port accessibility)
method_get_network_info() {
json_init
json_add_boolean "success" 1
local lan_ip
lan_ip=$(get_lan_ip)
json_add_string "lan_ip" "$lan_ip"
# Get public IPv4
json_add_object "ipv4"
local public_ipv4
public_ipv4=$(get_public_ipv4)
if [ -n "$public_ipv4" ]; then
json_add_string "address" "$public_ipv4"
json_add_string "status" "ok"
# Reverse DNS
local rdns
rdns=$(get_reverse_dns "$public_ipv4")
[ -n "$rdns" ] && json_add_string "hostname" "$rdns"
else
json_add_string "status" "unavailable"
fi
json_close_object
# Get public IPv6
json_add_object "ipv6"
local public_ipv6
public_ipv6=$(get_public_ipv6)
if [ -n "$public_ipv6" ]; then
json_add_string "address" "$public_ipv6"
json_add_string "status" "ok"
# Reverse DNS
local rdns6
rdns6=$(get_reverse_dns "$public_ipv6")
[ -n "$rdns6" ] && json_add_string "hostname" "$rdns6"
else
json_add_string "status" "unavailable"
fi
json_close_object
# External port accessibility (from internet perspective)
json_add_object "external_ports"
if [ -n "$public_ipv4" ]; then
# Check port 80
json_add_object "http"
if check_external_port "$public_ipv4" 80; then
json_add_boolean "accessible" 1
json_add_string "status" "open"
else
json_add_boolean "accessible" 0
json_add_string "status" "blocked"
json_add_string "hint" "Check upstream router port forwarding"
fi
json_close_object
# Check port 443
json_add_object "https"
if check_external_port "$public_ipv4" 443; then
json_add_boolean "accessible" 1
json_add_string "status" "open"
else
json_add_boolean "accessible" 0
json_add_string "status" "blocked"
json_add_string "hint" "Check upstream router port forwarding"
fi
json_close_object
else
json_add_object "http"
json_add_string "status" "unknown"
json_add_string "error" "No public IP"
json_close_object
json_add_object "https"
json_add_string "status" "unknown"
json_add_string "error" "No public IP"
json_close_object
fi
json_close_object
# Local firewall status
json_add_object "firewall"
local http_open=0
local https_open=0
check_port_firewall_open 80 && http_open=1
check_port_firewall_open 443 && https_open=1
json_add_boolean "http_open" "$http_open"
json_add_boolean "https_open" "$https_open"
if [ "$http_open" = "1" ] && [ "$https_open" = "1" ]; then
json_add_string "status" "ok"
elif [ "$http_open" = "1" ] || [ "$https_open" = "1" ]; then
json_add_string "status" "partial"
else
json_add_string "status" "closed"
fi
json_close_object
# HAProxy status
json_add_object "haproxy"
if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then
json_add_string "status" "running"
else
json_add_string "status" "stopped"
fi
json_close_object
json_dump
}
# Check health status for a service
method_check_service_health() {
local service_id domain
@ -1003,14 +1169,46 @@ method_check_service_health() {
json_add_string "service_id" "$service_id"
json_add_string "domain" "$domain"
# DNS check
# Get public IPs for comparison
local public_ipv4 public_ipv6
public_ipv4=$(get_public_ipv4)
public_ipv6=$(get_public_ipv6)
# Public IP info
json_add_object "public_ip"
json_add_string "ipv4" "$public_ipv4"
[ -n "$public_ipv6" ] && json_add_string "ipv6" "$public_ipv6"
if [ -n "$public_ipv4" ]; then
local rdns
rdns=$(get_reverse_dns "$public_ipv4")
[ -n "$rdns" ] && json_add_string "hostname" "$rdns"
fi
json_close_object
# DNS check with public IP comparison
json_add_object "dns"
if [ -n "$domain" ]; then
local resolved_ip
resolved_ip=$(check_dns_resolution "$domain")
if [ -n "$resolved_ip" ]; then
json_add_string "status" "ok"
json_add_string "resolved_ip" "$resolved_ip"
# Check if DNS points to public IP or private IP
case "$resolved_ip" in
10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.2*|172.30.*|172.31.*|192.168.*)
json_add_string "status" "private"
json_add_string "error" "DNS points to private IP (not reachable from internet)"
json_add_string "expected" "$public_ipv4"
;;
*)
if [ "$resolved_ip" = "$public_ipv4" ]; then
json_add_string "status" "ok"
else
json_add_string "status" "mismatch"
json_add_string "expected" "$public_ipv4"
json_add_string "hint" "DNS points to different IP than your public IP"
fi
;;
esac
else
json_add_string "status" "failed"
json_add_string "error" "DNS resolution failed"
@ -1020,6 +1218,30 @@ method_check_service_health() {
fi
json_close_object
# External port accessibility check
json_add_object "external_access"
if [ -n "$public_ipv4" ]; then
local http_ext=0
local https_ext=0
check_external_port "$public_ipv4" 80 && http_ext=1
check_external_port "$public_ipv4" 443 && https_ext=1
json_add_boolean "http_accessible" "$http_ext"
json_add_boolean "https_accessible" "$https_ext"
if [ "$http_ext" = "1" ] && [ "$https_ext" = "1" ]; then
json_add_string "status" "ok"
elif [ "$http_ext" = "1" ] || [ "$https_ext" = "1" ]; then
json_add_string "status" "partial"
json_add_string "hint" "Check upstream router/ISP port forwarding"
else
json_add_string "status" "blocked"
json_add_string "hint" "Ports not accessible from internet - check router/ISP"
fi
else
json_add_string "status" "unknown"
json_add_string "error" "Could not determine public IP"
fi
json_close_object
# Certificate check
json_add_object "certificate"
if [ -n "$domain" ]; then
@ -1293,6 +1515,7 @@ case "$1" in
"get_certificate_status": { "service_id": "string" },
"check_service_health": { "service_id": "string", "domain": "string" },
"check_all_health": {},
"get_network_info": {},
"get_landing_config": {},
"save_landing_config": { "auto_regen": "boolean" }
}
@ -1313,6 +1536,7 @@ EOF
get_certificate_status) method_get_certificate_status ;;
check_service_health) method_check_service_health ;;
check_all_health) method_check_all_health ;;
get_network_info) method_get_network_info ;;
get_landing_config) method_get_landing_config ;;
save_landing_config) method_save_landing_config ;;
*)

View File

@ -11,6 +11,7 @@
"get_certificate_status",
"check_service_health",
"check_all_health",
"get_network_info",
"get_landing_config"
],
"luci.haproxy": [