feat(web): integrate embedded web server, REST/SSE APIs and modernize configuration

- Add embedded KLALBWebServer with REST API, SSE streaming (200ms) and SPA hosting
- Introduce enableTUN configuration flag with fallback and non-admin execution support
- Refactor LineTable and ConnectLineTable to openConnections and autoConnections
- Rename denyLineTableQuery/Broadcast to denyConnectionQuery/Broadcast with backwards compatibility
- Support runtime config hot-reloading and automatic persistence to klalb-config.json
- Update default Web API port to 4665 and update dashboard submodule reference
This commit is contained in:
2026-08-24 00:01:24 +08:00
parent 041cc31c5f
commit 6c7017bc75
12 changed files with 1148 additions and 72 deletions
+9
View File
@@ -3,3 +3,12 @@
/klalbs4.json /klalbs4.json
/klalbs.json /klalbs.json
/klalbs2.json /klalbs2.json
# Dashboard / Frontend
dashboard/node_modules/
dashboard/dist/
dashboard/.pnpm-store/
.pnpm-debug.log*
dashboard/.env.local
dashboard/.env.*.local
+16 -2
View File
@@ -26,7 +26,7 @@ IDE metadata targets JDK 26 (`jdk-26.0.1`); the tree also compiles cleanly on JD
Runtime gotchas (all verified): Runtime gotchas (all verified):
- On JDK 25, `KNEOptimize.jar`'s `FastLib` reflects into `jdk.internal.misc.Unsafe`; without the two JVM flags above it throws `InaccessibleObjectException` at startup (app still runs). - On JDK 25, `KNEOptimize.jar`'s `FastLib` reflects into `jdk.internal.misc.Unsafe`; without the two JVM flags above it throws `InaccessibleObjectException` at startup (app still runs).
- Creating the SRv6 TUN adapter (`WintunCreateAdapter`) requires an elevated shell; without admin rights it logs "创建虚拟网卡失败" and continues with only the `inLoopBack` interface — links/bridges still work. - Creating the SRv6 TUN adapter (`WintunCreateAdapter`) requires an elevated shell; without admin rights it logs "创建虚拟网卡失败" and continues with only the `inLoopBack` interface — links/bridges still work.
- To disable TUN creation completely (e.g. for non-admin UI/routing testing), set `"TUNName": null` in `klalb-config.json` or leave the TUN Name empty in GUI settings. If omitted, it defaults to `"KLALB_SRv6"`. - To disable TUN creation completely (e.g. for non-admin UI/routing testing), set `"enableTUN": false` in `klalb-config.json` or toggle off "启用 TUN 虚拟网卡" in GUI/Web settings.
- Routing broadcast (`RouterInfo`) transmits `deviceName` across the network, which topology and node overview panels display. `deviceDescription` and `ExtraRoutes` remain local controller configs. - Routing broadcast (`RouterInfo`) transmits `deviceName` across the network, which topology and node overview panels display. `deviceDescription` and `ExtraRoutes` remain local controller configs.
## Verification ## Verification
@@ -35,7 +35,10 @@ No test suite, no CI. Classes named `*Test*` (`nathole/`, `ntp/`) are manual `ma
## Architecture ## Architecture
- Entrypoint `org.kne.cloud.network.klalb.KLALBMain`: load config → build `KLALBProxySystem` → open Swing GUI (`KLALBStateGUI3`) unless `"nogui": true` → interactive console (`help`, `links-state`, `route`, `kperf`, ...). - Entrypoint `org.kne.cloud.network.klalb.KLALBMain`: load config → build `KLALBProxySystem` → open Swing GUI (`KLALBStateGUI3`) unless `"nogui": true` start `KLALBWebServer` (if `"webUI": true` or web server enabled) → interactive console (`help`, `links-state`, `route`, `kperf`, ...).
- `org.kne.cloud.network.klalb.web.KLALBWebServer` — built-in HTTP/SSE server (JDK `HttpServer`):
- API endpoints: `/api/status`, `/api/events` (SSE stream, 200ms intervals), `/api/links`, `/api/links/action`, `/api/links/reconnect`, `/api/routes`, `/api/nodes` (topology graph), `/api/interfaces`, `/api/config`.
- Static file hosting / SPA fallback: serves `dashboard/dist/` assets if built.
- `org.kne.cloud.network` — generic socket framework: `VirtualSocket*` hierarchy, `SocketBridge` port-forwarding proxies, `ProtocolDetector` (multi-protocol mux on one port), `MultiProtocolSocketAddress` = URI-style addresses (`tcp://`, `udp://`, `kltp://`, `ntp://`) dispatched through the `SocketType` registry. - `org.kne.cloud.network` — generic socket framework: `VirtualSocket*` hierarchy, `SocketBridge` port-forwarding proxies, `ProtocolDetector` (multi-protocol mux on one port), `MultiProtocolSocketAddress` = URI-style addresses (`tcp://`, `udp://`, `kltp://`, `ntp://`) dispatched through the `SocketType` registry.
- `...network.klalb` — app core: `KLALBController` (the virtual SRv6 network), `KLALBRemoteLink` (WAN lines), `*Packet` wire-format classes, virtual socket implementations. - `...network.klalb` — app core: `KLALBController` (the virtual SRv6 network), `KLALBRemoteLink` (WAN lines), `*Packet` wire-format classes, virtual socket implementations.
- `...network.congestion` — pluggable congestion control (BBR, Vegas2, DCTCP...), chosen via `"congestionAlgorithm"` in config. - `...network.congestion` — pluggable congestion control (BBR, Vegas2, DCTCP...), chosen via `"congestionAlgorithm"` in config.
@@ -44,6 +47,17 @@ No test suite, no CI. Classes named `*Test*` (`nathole/`, `ntp/`) are manual `ma
- `...network.frpc` — frp client integration. - `...network.frpc` — frp client integration.
- `...klalb.ui` — all Swing UI code. - `...klalb.ui` — all Swing UI code.
## Frontend (Dashboard)
Located in `dashboard/`:
- **Stack**: Vite + React 19 + TypeScript + Tailwind CSS v4 + `@base-ui/react` (style: `base-nova`, icons: `lucide-react`).
- **Package Manager**: `pnpm` (run all commands from `dashboard/` directory).
- **Component installation**: **Must** use CLI via `pnpm dlx shadcn@latest add <component>` (e.g. `pnpm dlx shadcn@latest add alert card badge`). Never create or fake shadcn components manually.
- **Commands**:
- `pnpm dev` — Start Vite dev server (proxies to backend or connects to API on localhost).
- `pnpm build` — Typecheck and build SPA to `dashboard/dist` (which Java `KLALBWebServer` serves directly).
- `pnpm lint` / `pnpm typecheck` — Verification.
## Config ## Config
`klalb-config.json` is an array of items discriminated by their `"Type"` field. Adding a new item type requires a `KLALBConfigItem` subclass **plus** new cases in both `KLALBConfigItem.getDefaultJsonDeserializer()` and `getDefaultJsonSerializer()`; unknown types are preserved as `UnknownKLALBConfigItem`. Any Gson instance handling config must register these adapters via `registerToGsonBuilder` (see `KLALBProxySystem`). `klalb-config.json` is an array of items discriminated by their `"Type"` field. Adding a new item type requires a `KLALBConfigItem` subclass **plus** new cases in both `KLALBConfigItem.getDefaultJsonDeserializer()` and `getDefaultJsonSerializer()`; unknown types are preserved as `UnknownKLALBConfigItem`. Any Gson instance handling config must register these adapters via `registerToGsonBuilder` (see `KLALBProxySystem`).
+16 -10
View File
@@ -10,16 +10,19 @@
"TCPListen": "tcp://0.0.0.0:4565", "TCPListen": "tcp://0.0.0.0:4565",
"UDPListen": "udp://0.0.0.0:4572", "UDPListen": "udp://0.0.0.0:4572",
"VirtualSocketName": "kltp", "VirtualSocketName": "kltp",
"LineTable": [ "openConnections": [
"tcp://kne03.yoyo250.fun:4565", "tcp://kne03.yoyo250.fun:4565",
"tcp://kne04.yoyo250.fun:4565" "tcp://kne04.yoyo250.fun:4565",
],
"ConnectLineTable": [
"tcp://07f4acdef99b.ofalias.net:4565", "tcp://07f4acdef99b.ofalias.net:4565",
"tcp://kne01.yoyo250.fun:4565", "tcp://kne01.yoyo250.fun:4565",
"tcp://kne02.yoyo250.fun:4565" "tcp://kne02.yoyo250.fun:4565"
], ],
"ntpServerTable": [ "autoConnections": [
"tcp://07f4acdef99b.ofalias.net:4565",
"tcp://kne01.yoyo250.fun:4565",
"tcp://kne02.yoyo250.fun:4565"
],
"ntpServers": [
"ntp://ntp1.aliyun.com", "ntp://ntp1.aliyun.com",
"ntp://ntp2.aliyun.com", "ntp://ntp2.aliyun.com",
"ntp://ntp3.aliyun.com", "ntp://ntp3.aliyun.com",
@@ -38,8 +41,8 @@
"ntp://us.ntp.org.cn" "ntp://us.ntp.org.cn"
], ],
"ExtraRoutes": [], "ExtraRoutes": [],
"denyLineTableQuery": false, "denyConnectionQuery": false,
"denyLineTableBroadcast": false, "denyConnectionBroadcast": false,
"congestionAlgorithm": "BBR", "congestionAlgorithm": "BBR",
"burstLimit": 2.0, "burstLimit": 2.0,
"delayUpperBound": 1.2, "delayUpperBound": 1.2,
@@ -47,10 +50,13 @@
"nagleDelayTime": 1000000, "nagleDelayTime": 1000000,
"linkNagleDelayTime": 0, "linkNagleDelayTime": 0,
"linkConnectionsCount": 1, "linkConnectionsCount": 1,
"TUNName": null, "enableTUN": false,
"TUNName": "KLALB_SRv6",
"performanceStrategy": "multiscatter", "performanceStrategy": "multiscatter",
"DeviceName": "SerinaNya PC", "DeviceName": "Device Name",
"DeviceDescription": "desc", "DeviceDescription": "The Description of Device",
"webUI": true,
"webPort": 4665,
"NetworkInterfaceExcepts": [], "NetworkInterfaceExcepts": [],
"Type": "KLALBController" "Type": "KLALBController"
}, },
+11
View File
@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"shadcn": {
"source": "shadcn/ui",
"sourceType": "github",
"skillPath": "skills/shadcn/SKILL.md",
"computedHash": "c1a68ee06a668aced9ab2b5fbdea5f989864123794eb2e056b339a072dbb7f10"
}
}
}
@@ -210,7 +210,7 @@ public class KLALBController {
for (Iterator<IPMulticastDiscovery> iterator = ipmd.iterator(); iterator.hasNext(); ) { for (Iterator<IPMulticastDiscovery> iterator = ipmd.iterator(); iterator.hasNext(); ) {
IPMulticastDiscovery ipMulticastDiscovery = (IPMulticastDiscovery) iterator.next(); IPMulticastDiscovery ipMulticastDiscovery = (IPMulticastDiscovery) iterator.next();
if (ipMulticastDiscovery.isClosed() || (!ipMulticastDiscovery.getInterface().isUp()) if (ipMulticastDiscovery.isClosed() || (!ipMulticastDiscovery.getInterface().isUp())
|| (configItem != null && configItem.isDenyLineTableBroadcast())) { || (configItem != null && configItem.isDenyConnectionBroadcast())) {
iterator.remove(); iterator.remove();
try { try {
ipMulticastDiscovery.close(); ipMulticastDiscovery.close();
@@ -222,7 +222,7 @@ public class KLALBController {
} }
} }
if (configItem != null && configItem.isDenyLineTableBroadcast()) { if (configItem != null && configItem.isDenyConnectionBroadcast()) {
} else { } else {
List<NetworkInterface> interfaceList = networkInterfaceManager.getAllAvaliableNetworkInterface(); List<NetworkInterface> interfaceList = networkInterfaceManager.getAllAvaliableNetworkInterface();
@@ -647,8 +647,9 @@ public class KLALBController {
rawPortBinder= new PortBinder(this.getSelf().getAddress()); rawPortBinder= new PortBinder(this.getSelf().getAddress());
System.out.println(" Loaded: SRv6 Stack"); System.out.println(" Loaded: SRv6 Stack");
String name=(configItem!=null)?configItem.getTUNName():CONST.KLALB_S_RV6; String name = (configItem != null && configItem.getTUNName() != null) ? configItem.getTUNName() : CONST.KLALB_S_RV6;
boolean enableTUN = enableVirtualAdapter && (name != null) && !name.trim().isEmpty() && !name.trim().equalsIgnoreCase("null"); boolean isEnabled = configItem == null || configItem.isEnableTUN();
boolean enableTUN = enableVirtualAdapter && isEnabled && (name != null) && !name.trim().isEmpty() && !name.trim().equalsIgnoreCase("null");
if (enableTUN) { if (enableTUN) {
Thread t=new Thread(()->{ Thread t=new Thread(()->{
try { try {
@@ -800,18 +801,18 @@ public class KLALBController {
getIpv6Router().setASN(vasn); getIpv6Router().setASN(vasn);
} }
List<MultiProtocolSocketAddress> linele = configItem.getLineTable(); List<MultiProtocolSocketAddress> linele = configItem.getOpenConnections();
if (linele != null) { if (linele != null) {
getSelflineTable().addAll(linele); getSelflineTable().addAll(linele);
} }
List<MultiProtocolSocketAddress> linetoc = configItem.getConnectLineTable(); List<MultiProtocolSocketAddress> linetoc = configItem.getAutoConnections();
if (linetoc != null) { if (linetoc != null) {
linetoc.forEach((aline) -> { linetoc.forEach((aline) -> {
addRemoteLines(aline); addRemoteLines(aline);
}); });
} }
List<MultiProtocolSocketAddress> ntps = configItem.getNtpServerTable(); List<MultiProtocolSocketAddress> ntps = configItem.getNtpServers();
if (ntps != null) { if (ntps != null) {
getNTPTable().addAll(ntps); getNTPTable().addAll(ntps);
} }
@@ -16,12 +16,12 @@ public class KLALBControllerConfigItem extends KLALBConfigItem {
private MultiProtocolSocketAddress TCPListen=new MultiProtocolSocketAddress("0.0.0.0",4565); private MultiProtocolSocketAddress TCPListen=new MultiProtocolSocketAddress("0.0.0.0",4565);
private MultiProtocolSocketAddress UDPListen=new MultiProtocolSocketAddress("udp","0.0.0.0",4565); private MultiProtocolSocketAddress UDPListen=new MultiProtocolSocketAddress("udp","0.0.0.0",4565);
private String VirtualSocketName; private String VirtualSocketName;
private List<MultiProtocolSocketAddress>LineTable=new ArrayList<>(); private List<MultiProtocolSocketAddress> openConnections = new ArrayList<>();
private List<MultiProtocolSocketAddress>ConnectLineTable=new ArrayList<>(); private List<MultiProtocolSocketAddress> autoConnections = new ArrayList<>();
private List<MultiProtocolSocketAddress>ntpServerTable=new ArrayList<>(); private List<MultiProtocolSocketAddress> ntpServers = new ArrayList<>();
private List<String>ExtraRoutes=new ArrayList<>(); private List<String> ExtraRoutes = new ArrayList<>();
private boolean denyLineTableQuery=false; private boolean denyConnectionQuery = false;
private boolean denyLineTableBroadcast=false; private boolean denyConnectionBroadcast = false;
private String congestionAlgorithm="BBR"; private String congestionAlgorithm="BBR";
private double burstLimit=1.50; private double burstLimit=1.50;
private double delayUpperBound=1.20; private double delayUpperBound=1.20;
@@ -29,10 +29,37 @@ public class KLALBControllerConfigItem extends KLALBConfigItem {
private long nagleDelayTime=1000000L; private long nagleDelayTime=1000000L;
private long linkNagleDelayTime=1000000L; private long linkNagleDelayTime=1000000L;
private int linkConnectionsCount=1; private int linkConnectionsCount=1;
private boolean enableTUN = true;
private String TUNName=CONST.KLALB_S_RV6; private String TUNName=CONST.KLALB_S_RV6;
private String performanceStrategy="multifill"; private String performanceStrategy="multifill";
private String DeviceName; private String DeviceName;
private String DeviceDescription; private String DeviceDescription;
private boolean webUI = false;
private int webPort = 4665;
public boolean isEnableTUN() {
return enableTUN;
}
public void setEnableTUN(boolean enableTUN) {
this.enableTUN = enableTUN;
}
public boolean isWebUI() {
return webUI;
}
public void setWebUI(boolean webUI) {
this.webUI = webUI;
}
public int getWebPort() {
return webPort;
}
public void setWebPort(int webPort) {
this.webPort = webPort;
}
public String getPerformanceStrategy() { public String getPerformanceStrategy() {
return performanceStrategy; return performanceStrategy;
@@ -177,39 +204,52 @@ public class KLALBControllerConfigItem extends KLALBConfigItem {
public List<MultiProtocolSocketAddress> getLineTable() { public List<MultiProtocolSocketAddress> getOpenConnections() {
return LineTable; return openConnections;
} }
public void setOpenConnections(List<MultiProtocolSocketAddress> openConnections) {
this.openConnections = openConnections;
}
public List<MultiProtocolSocketAddress> getLineTable() {
return openConnections;
}
public void setLineTable(List<MultiProtocolSocketAddress> lineTable) { public void setLineTable(List<MultiProtocolSocketAddress> lineTable) {
LineTable = lineTable; this.openConnections = lineTable;
} }
public List<MultiProtocolSocketAddress> getAutoConnections() {
return autoConnections;
}
public void setAutoConnections(List<MultiProtocolSocketAddress> autoConnections) {
this.autoConnections = autoConnections;
}
public List<MultiProtocolSocketAddress> getConnectLineTable() { public List<MultiProtocolSocketAddress> getConnectLineTable() {
return ConnectLineTable; return autoConnections;
} }
public void setConnectLineTable(List<MultiProtocolSocketAddress> connectLineTable) { public void setConnectLineTable(List<MultiProtocolSocketAddress> connectLineTable) {
ConnectLineTable = connectLineTable; this.autoConnections = connectLineTable;
} }
public List<MultiProtocolSocketAddress> getNtpServers() {
return ntpServers;
}
public void setNtpServers(List<MultiProtocolSocketAddress> ntpServers) {
this.ntpServers = ntpServers;
}
public List<MultiProtocolSocketAddress> getNtpServerTable() { public List<MultiProtocolSocketAddress> getNtpServerTable() {
return ntpServerTable; return ntpServers;
} }
public void setNtpServerTable(List<MultiProtocolSocketAddress> ntpServerTable) { public void setNtpServerTable(List<MultiProtocolSocketAddress> ntpServerTable) {
this.ntpServerTable = ntpServerTable; this.ntpServers = ntpServerTable;
} }
public List<String> getExtraRoutes() { public List<String> getExtraRoutes() {
@@ -280,23 +320,36 @@ public class KLALBControllerConfigItem extends KLALBConfigItem {
public boolean isDenyLineTableQuery() { public boolean isDenyConnectionQuery() {
return denyLineTableQuery; return denyConnectionQuery;
} }
public void setDenyConnectionQuery(boolean denyConnectionQuery) {
this.denyConnectionQuery = denyConnectionQuery;
}
public boolean isDenyLineTableQuery() {
return denyConnectionQuery;
}
public void setDenyLineTableQuery(boolean denyLineTableQuery) { public void setDenyLineTableQuery(boolean denyLineTableQuery) {
this.denyLineTableQuery = denyLineTableQuery; this.denyConnectionQuery = denyLineTableQuery;
} }
public boolean isDenyConnectionBroadcast() {
return denyConnectionBroadcast;
}
public void setDenyConnectionBroadcast(boolean denyConnectionBroadcast) {
this.denyConnectionBroadcast = denyConnectionBroadcast;
}
public boolean isDenyLineTableBroadcast() { public boolean isDenyLineTableBroadcast() {
return denyLineTableBroadcast; return denyConnectionBroadcast;
} }
public void setDenyLineTableBroadcast(boolean denyLineTableBroadcast) { public void setDenyLineTableBroadcast(boolean denyLineTableBroadcast) {
this.denyLineTableBroadcast = denyLineTableBroadcast; this.denyConnectionBroadcast = denyLineTableBroadcast;
} }
@@ -352,12 +405,12 @@ public class KLALBControllerConfigItem extends KLALBConfigItem {
", TCPListen=" + TCPListen + ", TCPListen=" + TCPListen +
", UDPListen=" + UDPListen + ", UDPListen=" + UDPListen +
", VirtualSocketName='" + VirtualSocketName + '\'' + ", VirtualSocketName='" + VirtualSocketName + '\'' +
", LineTable=" + LineTable + ", openConnections=" + openConnections +
", ConnectLineTable=" + ConnectLineTable + ", autoConnections=" + autoConnections +
", ntpServerTable=" + ntpServerTable + ", ntpServers=" + ntpServers +
", ExtraRoutes=" + ExtraRoutes + ", ExtraRoutes=" + ExtraRoutes +
", denyLineTableQuery=" + denyLineTableQuery + ", denyConnectionQuery=" + denyConnectionQuery +
", denyLineTableBroadcast=" + denyLineTableBroadcast + ", denyConnectionBroadcast=" + denyConnectionBroadcast +
", congestionAlgorithm='" + congestionAlgorithm + '\'' + ", congestionAlgorithm='" + congestionAlgorithm + '\'' +
", burstLimit=" + burstLimit + ", burstLimit=" + burstLimit +
", delayUpperBound=" + delayUpperBound + ", delayUpperBound=" + delayUpperBound +
@@ -365,6 +418,7 @@ public class KLALBControllerConfigItem extends KLALBConfigItem {
", nagleDelayTime=" + nagleDelayTime + ", nagleDelayTime=" + nagleDelayTime +
", linkNagleDelayTime=" + linkNagleDelayTime + ", linkNagleDelayTime=" + linkNagleDelayTime +
", linkConnectionsCount=" + linkConnectionsCount + ", linkConnectionsCount=" + linkConnectionsCount +
", enableTUN=" + enableTUN +
", TUNName='" + TUNName + '\'' + ", TUNName='" + TUNName + '\'' +
", performanceStrategy='" + performanceStrategy + '\'' + ", performanceStrategy='" + performanceStrategy + '\'' +
", DeviceName='" + DeviceName + '\'' + ", DeviceName='" + DeviceName + '\'' +
@@ -378,11 +432,11 @@ public class KLALBControllerConfigItem extends KLALBConfigItem {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false; if (!super.equals(o)) return false;
KLALBControllerConfigItem that = (KLALBControllerConfigItem) o; KLALBControllerConfigItem that = (KLALBControllerConfigItem) o;
return nogui == that.nogui && denyLineTableQuery == that.denyLineTableQuery && denyLineTableBroadcast == that.denyLineTableBroadcast && Double.compare(burstLimit, that.burstLimit) == 0 && Double.compare(delayUpperBound, that.delayUpperBound) == 0 && Double.compare(delayLowerBound, that.delayLowerBound) == 0 && nagleDelayTime == that.nagleDelayTime && linkNagleDelayTime == that.linkNagleDelayTime && linkConnectionsCount == that.linkConnectionsCount && Objects.equals(language, that.language) && Objects.equals(VirtualAddress, that.VirtualAddress) && Objects.equals(VirtualASN, that.VirtualASN) && Objects.equals(DNS, that.DNS) && Objects.equals(TCPListen, that.TCPListen) && Objects.equals(UDPListen, that.UDPListen) && Objects.equals(VirtualSocketName, that.VirtualSocketName) && Objects.equals(LineTable, that.LineTable) && Objects.equals(ConnectLineTable, that.ConnectLineTable) && Objects.equals(ntpServerTable, that.ntpServerTable) && Objects.equals(ExtraRoutes, that.ExtraRoutes) && Objects.equals(congestionAlgorithm, that.congestionAlgorithm) && Objects.equals(TUNName, that.TUNName) && Objects.equals(performanceStrategy, that.performanceStrategy) && Objects.equals(DeviceName, that.DeviceName) && Objects.equals(DeviceDescription, that.DeviceDescription) && Objects.equals(NetworkInterfaceExcepts, that.NetworkInterfaceExcepts); return nogui == that.nogui && enableTUN == that.enableTUN && denyConnectionQuery == that.denyConnectionQuery && denyConnectionBroadcast == that.denyConnectionBroadcast && Double.compare(burstLimit, that.burstLimit) == 0 && Double.compare(delayUpperBound, that.delayUpperBound) == 0 && Double.compare(delayLowerBound, that.delayLowerBound) == 0 && nagleDelayTime == that.nagleDelayTime && linkNagleDelayTime == that.linkNagleDelayTime && linkConnectionsCount == that.linkConnectionsCount && Objects.equals(language, that.language) && Objects.equals(VirtualAddress, that.VirtualAddress) && Objects.equals(VirtualASN, that.VirtualASN) && Objects.equals(DNS, that.DNS) && Objects.equals(TCPListen, that.TCPListen) && Objects.equals(UDPListen, that.UDPListen) && Objects.equals(VirtualSocketName, that.VirtualSocketName) && Objects.equals(openConnections, that.openConnections) && Objects.equals(autoConnections, that.autoConnections) && Objects.equals(ntpServers, that.ntpServers) && Objects.equals(ExtraRoutes, that.ExtraRoutes) && Objects.equals(congestionAlgorithm, that.congestionAlgorithm) && Objects.equals(TUNName, that.TUNName) && Objects.equals(performanceStrategy, that.performanceStrategy) && Objects.equals(DeviceName, that.DeviceName) && Objects.equals(DeviceDescription, that.DeviceDescription) && Objects.equals(NetworkInterfaceExcepts, that.NetworkInterfaceExcepts);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(super.hashCode(), language, nogui, VirtualAddress, VirtualASN, DNS, TCPListen, UDPListen, VirtualSocketName, LineTable, ConnectLineTable, ntpServerTable, ExtraRoutes, denyLineTableQuery, denyLineTableBroadcast, congestionAlgorithm, burstLimit, delayUpperBound, delayLowerBound, nagleDelayTime, linkNagleDelayTime, linkConnectionsCount, TUNName, performanceStrategy, DeviceName, DeviceDescription, NetworkInterfaceExcepts); return Objects.hash(super.hashCode(), language, nogui, VirtualAddress, VirtualASN, DNS, TCPListen, UDPListen, VirtualSocketName, openConnections, autoConnections, ntpServers, ExtraRoutes, denyConnectionQuery, denyConnectionBroadcast, congestionAlgorithm, burstLimit, delayUpperBound, delayLowerBound, nagleDelayTime, linkNagleDelayTime, linkConnectionsCount, enableTUN, TUNName, performanceStrategy, DeviceName, DeviceDescription, NetworkInterfaceExcepts);
} }
} }
@@ -52,6 +52,13 @@ public class KLALBMain {
}catch(Throwable e) { }catch(Throwable e) {
e.printStackTrace(); e.printStackTrace();
} }
try {
if(kpcje.getControllerConfig() != null && kpcje.getControllerConfig().isWebUI()) {
kpcje.enableWebServer();
}
} catch(Throwable e) {
System.err.println("Failed to start web server: " + e.getMessage());
}
dtb.putTime("UI"); dtb.putTime("UI");
//dtb.print(); //dtb.print();
/*MultipurposeSocketAddress mpa=new MultipurposeSocketAddress("127.9.9.9", 49573); /*MultipurposeSocketAddress mpa=new MultipurposeSocketAddress("127.9.9.9", 49573);
@@ -84,6 +91,7 @@ public class KLALBMain {
case "help": case "help":
System.out.println(" help / ?: see help"); System.out.println(" help / ?: see help");
System.out.println(" monitor: show monitor GUI"); System.out.println(" monitor: show monitor GUI");
System.out.println(" web [start|stop|status <port>]: manage web dashboard");
System.out.println(" links-state: query link states"); System.out.println(" links-state: query link states");
System.out.println(" links-add <addr:port>: add link"); System.out.println(" links-add <addr:port>: add link");
System.out.println(" links-remove <addr:port>: remove link"); System.out.println(" links-remove <addr:port>: remove link");
@@ -102,6 +110,42 @@ public class KLALBMain {
e.printStackTrace(); e.printStackTrace();
} }
break; break;
case "web":
if(sc.length >= 2) {
String action = sc[1].toLowerCase();
if("start".equals(action)) {
int p = 4665;
if(sc.length >= 3) {
try { p = Integer.parseInt(sc[2]); } catch (NumberFormatException ignored) {}
} else if(kpcje.getControllerConfig() != null && kpcje.getControllerConfig().getWebPort() > 0) {
p = kpcje.getControllerConfig().getWebPort();
}
try {
kpcje.enableWebServer(p);
System.out.println("Web dashboard started on http://localhost:" + p);
} catch(Exception e) {
System.out.println("Failed to start web server: " + e.getMessage());
}
} else if("stop".equals(action)) {
kpcje.disableWebServer();
System.out.println("Web dashboard stopped.");
} else if("status".equals(action)) {
if(kpcje.isWebServerEnabled()) {
System.out.println("Web dashboard is running on port " + kpcje.getWebServer().getPort());
} else {
System.out.println("Web dashboard is stopped.");
}
} else {
System.out.println("Usage: web [start|stop|status <port>]");
}
} else {
if(kpcje.isWebServerEnabled()) {
System.out.println("Web dashboard is running on port " + kpcje.getWebServer().getPort());
} else {
System.out.println("Web dashboard is not running. Use 'web start [port]' to start.");
}
}
break;
case "links-state": case "links-state":
System.out.println("links state"); System.out.println("links state");
//System.out.println("状态\t上传流量\t下载流量\t上传速度\t下载速度\t上传延迟\t下载延迟\t上传抖动\t下载抖动"); //System.out.println("状态\t上传流量\t下载流量\t上传速度\t下载速度\t上传延迟\t下载延迟\t上传抖动\t下载抖动");
@@ -5,25 +5,35 @@ import java.io.FileReader;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.net.InetSocketAddress; import java.lang.reflect.Type;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.HashSet; import java.util.HashSet;
import org.kne.cloud.network.*; import org.kne.cloud.network.*;
import org.kne.cloud.network.klalb.ui.KLALBStateGUI3; import org.kne.cloud.network.klalb.ui.KLALBStateGUI3;
import org.kne.cloud.network.klalb.ui.Language; import org.kne.cloud.network.klalb.ui.Language;
import org.kne.cloud.network.klalb.ui.UIEnv; import org.kne.cloud.network.klalb.ui.UIEnv;
import org.kne.cloud.network.klalb.web.KLALBWebServer;
import java.util.Set; import java.util.Set;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
public class KLALBProxySystem { public class KLALBProxySystem {
private Set<Proxy> proxys=new HashSet<>(); private Set<Proxy> proxys=new HashSet<>();
private KLALBController klalbController; private KLALBController klalbController;
private KLALBRemoteManagement krm; private KLALBRemoteManagement krm;
private KLALBWebServer webServer;
private KLALBConfig config; private KLALBConfig config;
private Gson gson; private Gson gson;
private File jsonFile; private File jsonFile;
@@ -31,8 +41,28 @@ public class KLALBProxySystem {
GsonBuilder gb=new GsonBuilder().setPrettyPrinting(); GsonBuilder gb=new GsonBuilder().setPrettyPrinting();
MultiProtocolSocketAddress.registerToGsonBuilder(gb); MultiProtocolSocketAddress.registerToGsonBuilder(gb);
KLALBConfigItem.registerToGsonBuilder(gb); KLALBConfigItem.registerToGsonBuilder(gb);
gb.registerTypeAdapter(InetAddress.class, new JsonSerializer<InetAddress>() {
@Override
public JsonElement serialize(InetAddress src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getHostAddress());
}
});
gb.registerTypeAdapter(InetAddress.class, new JsonDeserializer<InetAddress>() {
@Override
public InetAddress deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
return InetAddress.getByName(json.getAsString());
} catch (Exception e) {
throw new JsonParseException(e);
}
}
});
gson=gb.create(); gson=gb.create();
} }
public Gson getGson() {
return gson;
}
public Set<Proxy> getProxys() { public Set<Proxy> getProxys() {
return proxys; return proxys;
} }
@@ -81,6 +111,39 @@ public class KLALBProxySystem {
krm=null; krm=null;
} }
} }
public void enableWebServer() throws IOException {
int port = 4665;
KLALBControllerConfigItem cci = getControllerConfig();
if (cci != null && cci.getWebPort() > 0) {
port = cci.getWebPort();
}
enableWebServer(port);
}
public void enableWebServer(int port) throws IOException {
if (webServer == null) {
webServer = new KLALBWebServer(this, port);
webServer.start();
} else {
throw new IllegalStateException("Web server already enabled!");
}
}
public boolean isWebServerEnabled() {
return webServer != null && webServer.isRunning();
}
public KLALBWebServer getWebServer() {
return webServer;
}
public void disableWebServer() {
if (webServer != null) {
webServer.stop();
webServer = null;
}
}
public void loadConfigJson(File jsonFile) throws IOException { public void loadConfigJson(File jsonFile) throws IOException {
this.jsonFile=jsonFile; this.jsonFile=jsonFile;
@@ -195,30 +258,24 @@ public class KLALBProxySystem {
} }
public void saveConfigToFile() {
if (jsonFile != null && config != null) {
String json = gson.toJson(config);
try (FileWriter fw = new FileWriter(jsonFile)) {
fw.write(json);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private KLALBStateGUI3 kgui; private KLALBStateGUI3 kgui;
public KLALBStateGUI3 getKLALBGUI() { public KLALBStateGUI3 getKLALBGUI() {
if(kgui==null) { if(kgui==null) {
kgui=new KLALBStateGUI3(klalbController); kgui=new KLALBStateGUI3(klalbController);
kgui.loadConfig(config); kgui.loadConfig(config);
kgui.setSaveComsumer((cfg)->{ kgui.setSaveComsumer((cfg)->{
String json=gson.toJson(cfg); saveConfigToFile();
if(jsonFile!=null) {
FileWriter fw = null;
try {
fw=new FileWriter(jsonFile);
fw.write(json);
}catch(IOException e) {
e.printStackTrace();
}finally {
if(fw!=null)
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}); });
} }
return kgui; return kgui;
@@ -0,0 +1,876 @@
package org.kne.cloud.network.klalb.web;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import com.sun.net.httpserver.*;
import com.google.gson.*;
import org.kne.cloud.clock.HighAccuracyClock;
import org.kne.cloud.network.MultiProtocolSocketAddress;
import org.kne.cloud.network.ipv6.IPv6Address;
import org.kne.cloud.network.ipv6.IPv6AddressGroup;
import org.kne.cloud.network.ipv6.IPv6NetworkLink;
import org.kne.cloud.network.ipv6.RouteItem;
import org.kne.cloud.network.klalb.*;
import org.kne.cloud.network.monitor.LinkStatus;
import org.kne.cloud.network.srv6.KLALBRoutingProtocol;
import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection;
import org.kne.cloud.network.srv6.NeighborInfo;
import org.kne.cloud.network.srv6.RouterInfo;
import org.kne.cloud.network.srv6.SRv6Router;
/**
* KLALB Web Server providing REST API, SSE live event stream,
* and static dashboard hosting.
*/
public class KLALBWebServer {
private final KLALBProxySystem proxySystem;
private final int port;
private HttpServer server;
private final Gson gson;
private final ScheduledExecutorService sseExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "KLALB-Web-SSE");
t.setDaemon(true);
return t;
});
private final Set<HttpExchange> sseClients = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final AtomicBoolean running = new AtomicBoolean(false);
public KLALBWebServer(KLALBProxySystem proxySystem, int port) {
this.proxySystem = proxySystem;
this.port = port;
this.gson = proxySystem.getGson();
}
public synchronized void start() throws IOException {
if (running.get()) return;
server = HttpServer.create(new InetSocketAddress(port), 0);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
// API Contexts
server.createContext("/api/status", this::handleStatus);
server.createContext("/api/events", this::handleEvents);
server.createContext("/api/links", this::handleLinks);
server.createContext("/api/links/action", this::handleLinkAction);
server.createContext("/api/links/reconnect", this::handleReconnectAll);
server.createContext("/api/routes", this::handleRoutes);
server.createContext("/api/nodes", this::handleNodes);
server.createContext("/api/interfaces", this::handleInterfaces);
server.createContext("/api/config", this::handleConfig);
// Static Files / SPA Fallback Handler
server.createContext("/", this::handleStatic);
server.start();
running.set(true);
startSseBroadcaster();
System.out.println("KLALB Web Dashboard started at http://localhost:" + port);
}
public synchronized void stop() {
if (!running.get()) return;
running.set(false);
sseExecutor.shutdownNow();
for (HttpExchange client : sseClients) {
try {
client.close();
} catch (Exception ignored) {}
}
sseClients.clear();
if (server != null) {
server.stop(1);
server = null;
}
System.out.println("KLALB Web Dashboard stopped.");
}
public boolean isRunning() {
return running.get();
}
public int getPort() {
return port;
}
private void setCorsHeaders(HttpExchange exchange) {
Headers headers = exchange.getResponseHeaders();
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept");
}
private boolean handleCorsPreflight(HttpExchange exchange) throws IOException {
setCorsHeaders(exchange);
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(204, -1);
exchange.close();
return true;
}
return false;
}
private void sendJsonResponse(HttpExchange exchange, int statusCode, Object data) throws IOException {
setCorsHeaders(exchange);
byte[] bytes = gson.toJson(data).getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(statusCode, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
private void sendError(HttpExchange exchange, int statusCode, String message) throws IOException {
JsonObject error = new JsonObject();
error.addProperty("error", message);
sendJsonResponse(exchange, statusCode, error);
}
private String readRequestBody(HttpExchange exchange) throws IOException {
try (InputStream is = exchange.getRequestBody()) {
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
}
// -------------------------------------------------------------
// API Handlers
// -------------------------------------------------------------
private JsonObject getStatusJson() {
JsonObject root = new JsonObject();
KLALBController kc = proxySystem.getKlalbController();
if (kc == null) {
root.addProperty("status", "not_ready");
return root;
}
root.addProperty("version", CONST.klalbver);
root.addProperty("title", CONST.klalb);
IPv6AddressGroup self = kc.getSelf();
root.addProperty("address", self != null ? self.getAddress().toString() : "");
SRv6Router router = kc.getIpv6Router();
if (router != null && router.getKlalbRouteProtol() != null) {
root.addProperty("onlineDevices", router.getKlalbRouteProtol().getDevicesFound());
root.addProperty("deviceName", router.getDeviceName() != null ? router.getDeviceName() : "");
} else {
root.addProperty("onlineDevices", 0);
root.addProperty("deviceName", "");
}
KLALBControllerConfigItem configItem = proxySystem.getControllerConfig();
if (configItem != null) {
root.addProperty("deviceDescription", configItem.getDeviceDescription() != null ? configItem.getDeviceDescription() : "");
root.addProperty("enableTUN", configItem.isEnableTUN());
root.addProperty("tunName", configItem.isEnableTUN() && configItem.getTUNName() != null ? configItem.getTUNName() : "");
root.addProperty("congestionAlgorithm", configItem.getCongestionAlgorithm() != null ? configItem.getCongestionAlgorithm() : "");
root.addProperty("performanceStrategy", configItem.getPerformanceStrategy() != null ? configItem.getPerformanceStrategy() : "");
}
// Clock
HighAccuracyClock hac = kc.getClock();
if (hac != null) {
long delta = hac.getFrequency() - 1000000000;
double ppm = delta / 1000.0;
root.addProperty("timeStr", hac.toString2());
root.addProperty("timePpm", ppm);
}
// Metrics
JsonObject metrics = new JsonObject();
if (kc.getLinkMonitor() != null) {
metrics.addProperty("upSpeed", kc.getLinkMonitor().getOutSpeed());
metrics.addProperty("downSpeed", kc.getLinkMonitor().getInSpeed());
metrics.addProperty("upSpeedMax", kc.getLinkMonitor().getOutSpeedMax());
metrics.addProperty("downSpeedMax", kc.getLinkMonitor().getInSpeedMax());
metrics.addProperty("upPPS", kc.getLinkMonitor().getOutPPS());
metrics.addProperty("downPPS", kc.getLinkMonitor().getInPPS());
metrics.addProperty("upPPSMax", kc.getLinkMonitor().getOutPPSMax());
metrics.addProperty("downPPSMax", kc.getLinkMonitor().getInPPSMax());
metrics.addProperty("upTraffic", kc.getLinkMonitor().getOutTraffic());
metrics.addProperty("downTraffic", kc.getLinkMonitor().getInTraffic());
}
if (kc.getDatatMonitor() != null) {
metrics.addProperty("dataUpSpeed", kc.getDatatMonitor().getOutSpeed());
metrics.addProperty("dataDownSpeed", kc.getDatatMonitor().getInSpeed());
metrics.addProperty("dataUpPPS", kc.getDatatMonitor().getOutPPS());
metrics.addProperty("dataDownPPS", kc.getDatatMonitor().getInPPS());
metrics.addProperty("dataUpTraffic", kc.getDatatMonitor().getOutTraffic());
metrics.addProperty("dataDownTraffic", kc.getDatatMonitor().getInTraffic());
}
if (router != null) {
metrics.addProperty("backplaneDelay", router.getBackplaneTime());
metrics.addProperty("backplanePPS", router.getBackplanePPS());
metrics.addProperty("backplanePPSMax", router.getBackplanePPSMax());
metrics.addProperty("backplaneECNRate", router.getECNRate());
metrics.addProperty("backplaneLossRate", router.getLossRate());
}
root.add("metrics", metrics);
return root;
}
private void handleStatus(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
sendError(exchange, 405, "Method not allowed");
return;
}
sendJsonResponse(exchange, 200, getStatusJson());
}
private JsonArray getLinksJson() {
JsonArray array = new JsonArray();
KLALBController kc = proxySystem.getKlalbController();
if (kc == null) return array;
List<IPv6NetworkLink> linesList = kc.getLines();
synchronized (linesList) {
for (IPv6NetworkLink link : linesList) {
if (!(link instanceof KLALBRemoteLink)) continue;
KLALBRemoteLink remoteLink = (KLALBRemoteLink) link;
JsonObject obj = new JsonObject();
obj.addProperty("name", remoteLink.getName());
obj.addProperty("state", LinkStatus.stateToString(remoteLink.getState()));
obj.addProperty("stateCode", remoteLink.getState());
MultiProtocolSocketAddress mpsa = remoteLink.getSocketAddress();
obj.addProperty("socketAddress", mpsa != null ? mpsa.toString() : "");
IPv6AddressGroup vaddr = remoteLink.getRemoteVaddr();
obj.addProperty("vaddr", vaddr != null ? vaddr.getAddress().toString() : "");
if (remoteLink.getMonitor() != null) {
obj.addProperty("upSpeed", remoteLink.getMonitor().getOutSpeed());
obj.addProperty("downSpeed", remoteLink.getMonitor().getInSpeed());
obj.addProperty("upPPS", remoteLink.getMonitor().getOutPPS());
obj.addProperty("downPPS", remoteLink.getMonitor().getInPPS());
obj.addProperty("upTraffic", remoteLink.getMonitor().getOutTraffic());
obj.addProperty("downTraffic", remoteLink.getMonitor().getInTraffic());
obj.addProperty("upDelay", remoteLink.getMonitor().getOutDelay());
obj.addProperty("downDelay", remoteLink.getMonitor().getInDelay());
obj.addProperty("upDelayMin", remoteLink.getMonitor().getOutDelayMin());
obj.addProperty("downDelayMin", remoteLink.getMonitor().getInDelayMin());
obj.addProperty("upJitter", remoteLink.getMonitor().getOutJitter());
obj.addProperty("downJitter", remoteLink.getMonitor().getInJitter());
}
array.add(obj);
}
}
return array;
}
private void handleLinks(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
String method = exchange.getRequestMethod();
KLALBController kc = proxySystem.getKlalbController();
if ("GET".equalsIgnoreCase(method)) {
sendJsonResponse(exchange, 200, getLinksJson());
} else if ("POST".equalsIgnoreCase(method)) {
String body = readRequestBody(exchange);
try {
JsonObject req = new JsonParser().parse(body).getAsJsonObject();
String address = req.get("address").getAsString().trim();
if (address.isEmpty()) {
sendError(exchange, 400, "Address is required");
return;
}
MultiProtocolSocketAddress mpsa = new MultiProtocolSocketAddress(address);
List<KLALBRemoteLink> added = kc.addRemoteLines(mpsa);
JsonObject resp = new JsonObject();
resp.addProperty("success", !added.isEmpty());
resp.addProperty("count", added.size());
sendJsonResponse(exchange, 200, resp);
} catch (Exception e) {
sendError(exchange, 400, "Failed to add link: " + e.getMessage());
}
} else if ("DELETE".equalsIgnoreCase(method)) {
String query = exchange.getRequestURI().getQuery();
String address = null;
if (query != null && query.startsWith("address=")) {
address = URLDecoder.decode(query.substring(8), StandardCharsets.UTF_8);
} else {
String body = readRequestBody(exchange);
if (!body.isEmpty()) {
try {
JsonObject req = new JsonParser().parse(body).getAsJsonObject();
if (req.has("address")) {
address = req.get("address").getAsString();
}
} catch (Exception ignored) {}
}
}
if (address == null || address.trim().isEmpty()) {
sendError(exchange, 400, "Address parameter required");
return;
}
try {
MultiProtocolSocketAddress mpsa = new MultiProtocolSocketAddress(address.trim());
List<KLALBRemoteLink> removed = kc.removeRemoteLines(mpsa);
JsonObject resp = new JsonObject();
resp.addProperty("success", !removed.isEmpty());
resp.addProperty("count", removed.size());
sendJsonResponse(exchange, 200, resp);
} catch (Exception e) {
sendError(exchange, 400, "Failed to remove link: " + e.getMessage());
}
} else {
sendError(exchange, 405, "Method not allowed");
}
}
private void handleLinkAction(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
sendError(exchange, 405, "Method not allowed");
return;
}
String body = readRequestBody(exchange);
try {
JsonObject req = new JsonParser().parse(body).getAsJsonObject();
String action = req.get("action").getAsString();
String address = req.has("address") ? req.get("address").getAsString() : null;
KLALBController kc = proxySystem.getKlalbController();
KLALBRemoteLink target = null;
if (address != null) {
for (IPv6NetworkLink link : kc.getLines()) {
if (link instanceof KLALBRemoteLink rl) {
if (rl.getName().equalsIgnoreCase(address) ||
(rl.getSocketAddress() != null && rl.getSocketAddress().toString().equalsIgnoreCase(address))) {
target = rl;
break;
}
}
}
}
JsonObject resp = new JsonObject();
if (target != null) {
switch (action) {
case "reconnect" -> {
target.reconnectImmediately();
resp.addProperty("success", true);
}
case "disconnect" -> {
target.dislink();
resp.addProperty("success", true);
}
case "remove" -> {
MultiProtocolSocketAddress mtar = target.getSocketAddress();
if (mtar != null) {
kc.removeRemoteLines(mtar);
} else {
target.close();
}
resp.addProperty("success", true);
}
default -> {
sendError(exchange, 400, "Unknown action: " + action);
return;
}
}
} else {
sendError(exchange, 404, "Target link not found: " + address);
return;
}
sendJsonResponse(exchange, 200, resp);
} catch (Exception e) {
sendError(exchange, 400, "Failed to execute link action: " + e.getMessage());
}
}
private void handleReconnectAll(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
sendError(exchange, 405, "Method not allowed");
return;
}
KLALBController kc = proxySystem.getKlalbController();
if (kc != null) {
kc.reconnectImmediately();
JsonObject resp = new JsonObject();
resp.addProperty("success", true);
sendJsonResponse(exchange, 200, resp);
} else {
sendError(exchange, 500, "Controller not ready");
}
}
private void handleRoutes(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
sendError(exchange, 405, "Method not allowed");
return;
}
KLALBController kc = proxySystem.getKlalbController();
JsonArray routesArray = new JsonArray();
if (kc != null && kc.getIpv6Router() != null) {
List<RouteItem> list = new ArrayList<>(kc.getIpv6Router().getCurrentRouteTabel());
Collections.sort(list);
for (RouteItem item : list) {
JsonObject obj = new JsonObject();
obj.addProperty("destination", item.getDestination() != null ? item.getDestination().toString() : "");
obj.addProperty("protocol", item.getProto());
obj.addProperty("preference", item.getPre());
obj.addProperty("cost", item.getCost());
obj.addProperty("flag", item.getFlag());
obj.addProperty("nexthop", item.getNexthop() != null ? item.getNexthop().toString() : "");
obj.addProperty("interface", item.getDestlink() != null ? item.getDestlink().getName() : "");
routesArray.add(obj);
}
}
sendJsonResponse(exchange, 200, routesArray);
}
private void handleNodes(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
sendError(exchange, 405, "Method not allowed");
return;
}
KLALBController kc = proxySystem.getKlalbController();
JsonObject result = new JsonObject();
JsonArray nodesArray = new JsonArray();
JsonArray edgesArray = new JsonArray();
if (kc != null && kc.getIpv6Router() != null && kc.getIpv6Router().getKlalbRouteProtol() != null) {
KLALBRoutingProtocol rproto = kc.getIpv6Router().getKlalbRouteProtol();
Map<IPv6Address, Long> addrs = rproto.getAddresses();
IPv6Address selfAddr = kc.getIpv6Router().getLocator().getAddress();
if (addrs != null) {
for (IPv6Address addr : addrs.keySet()) {
JsonObject nodeObj = new JsonObject();
nodeObj.addProperty("id", addr.toString());
nodeObj.addProperty("address", addr.toString());
nodeObj.addProperty("compressedAddress", addr.toCompressedString());
nodeObj.addProperty("isSelf", addr.equals(selfAddr));
String dname = rproto.getDeviceName(addr);
nodeObj.addProperty("deviceName", dname != null ? dname : "");
nodesArray.add(nodeObj);
}
}
Map<IPv6Address, RouterInfo> netmap = rproto.getNetmap();
if (netmap != null) {
Set<String> seenEdges = new HashSet<>();
for (Map.Entry<IPv6Address, RouterInfo> entry : netmap.entrySet()) {
IPv6Address from = entry.getKey();
RouterInfo info = entry.getValue();
if (info != null && info.getNeighborAddresses() != null) {
for (NeighborInfo nb : info.getNeighborAddresses()) {
if (nb != null && nb.getLocator() != null) {
String to = nb.getLocator().getAddress().toString();
String edgeKey = from.toString().compareTo(to) < 0
? from.toString() + "->" + to
: to + "->" + from.toString();
if (!seenEdges.contains(edgeKey)) {
seenEdges.add(edgeKey);
JsonObject edgeObj = new JsonObject();
edgeObj.addProperty("source", from.toString());
edgeObj.addProperty("target", to);
edgeObj.addProperty("delay", nb.getUploadDelay());
edgeObj.addProperty("cost", nb.getUploadDelayMin());
edgesArray.add(edgeObj);
}
}
}
}
}
}
}
result.add("nodes", nodesArray);
result.add("edges", edgesArray);
sendJsonResponse(exchange, 200, result);
}
private void handleInterfaces(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
sendError(exchange, 405, "Method not allowed");
return;
}
KLALBController kc = proxySystem.getKlalbController();
JsonArray ifacesArray = new JsonArray();
if (kc != null && kc.getNetworkInterfaceManager() != null) {
List<NetworkInterface> ifaces = kc.getNetworkInterfaceManager().getAllAvaliableNetworkInterface();
for (NetworkInterface nif : ifaces) {
JsonObject obj = new JsonObject();
obj.addProperty("name", nif.getName());
obj.addProperty("displayName", nif.getDisplayName());
JsonArray ips = new JsonArray();
List<InetAddress> addrs = kc.getNetworkInterfaceManager().getNetworkInterfaceAddress(nif);
for (InetAddress a : addrs) {
ips.add(new JsonPrimitive(a.getHostAddress()));
}
obj.add("addresses", ips);
ifacesArray.add(obj);
}
}
sendJsonResponse(exchange, 200, ifacesArray);
}
private void handleConfig(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
String method = exchange.getRequestMethod();
if ("GET".equalsIgnoreCase(method)) {
KLALBControllerConfigItem config = proxySystem.getControllerConfig();
if (config != null) {
sendJsonResponse(exchange, 200, config);
} else {
sendError(exchange, 404, "Configuration not found");
}
} else if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) {
String body = readRequestBody(exchange);
try {
JsonObject json = new JsonParser().parse(body).getAsJsonObject();
KLALBControllerConfigItem current = proxySystem.getControllerConfig();
if (current != null) {
if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) {
current.setDeviceName(json.get("deviceName").getAsString());
} else if (json.has("DeviceName") && !json.get("DeviceName").isJsonNull()) {
current.setDeviceName(json.get("DeviceName").getAsString());
}
if (json.has("deviceDescription") && !json.get("deviceDescription").isJsonNull()) {
current.setDeviceDescription(json.get("deviceDescription").getAsString());
} else if (json.has("DeviceDescription") && !json.get("DeviceDescription").isJsonNull()) {
current.setDeviceDescription(json.get("DeviceDescription").getAsString());
}
if (json.has("language") && !json.get("language").isJsonNull()) {
current.setLanguage(json.get("language").getAsString());
}
if (json.has("virtualAddress") && !json.get("virtualAddress").isJsonNull()) {
current.setVirtualAddress(json.get("virtualAddress").getAsString());
} else if (json.has("VirtualAddress") && !json.get("VirtualAddress").isJsonNull()) {
current.setVirtualAddress(json.get("VirtualAddress").getAsString());
}
if (json.has("virtualASN") && !json.get("virtualASN").isJsonNull()) {
current.setVirtualASN(json.get("virtualASN").getAsLong());
} else if (json.has("VirtualASN") && !json.get("VirtualASN").isJsonNull()) {
current.setVirtualASN(json.get("VirtualASN").getAsLong());
}
if (json.has("virtualSocketName") && !json.get("virtualSocketName").isJsonNull()) {
current.setVirtualSocketName(json.get("virtualSocketName").getAsString());
} else if (json.has("VirtualSocketName") && !json.get("VirtualSocketName").isJsonNull()) {
current.setVirtualSocketName(json.get("VirtualSocketName").getAsString());
}
if (json.has("enableTUN")) {
current.setEnableTUN(json.get("enableTUN").getAsBoolean());
} else if (json.has("EnableTUN")) {
current.setEnableTUN(json.get("EnableTUN").getAsBoolean());
}
if (json.has("tunName") && !json.get("tunName").isJsonNull()) {
current.setTUNName(json.get("tunName").getAsString());
} else if (json.has("TUNName") && !json.get("TUNName").isJsonNull()) {
current.setTUNName(json.get("TUNName").getAsString());
}
if (json.has("webUI")) {
current.setWebUI(json.get("webUI").getAsBoolean());
}
if (json.has("webPort")) {
current.setWebPort(json.get("webPort").getAsInt());
}
if (json.has("nogui")) {
current.setNogui(json.get("nogui").getAsBoolean());
}
if (json.has("tcpListen") && !json.get("tcpListen").isJsonNull()) {
current.setTCPListen(new MultiProtocolSocketAddress(json.get("tcpListen").getAsString()));
} else if (json.has("TCPListen") && !json.get("TCPListen").isJsonNull()) {
current.setTCPListen(new MultiProtocolSocketAddress(json.get("TCPListen").getAsString()));
}
if (json.has("udpListen") && !json.get("udpListen").isJsonNull()) {
current.setUDPListen(new MultiProtocolSocketAddress(json.get("udpListen").getAsString()));
} else if (json.has("UDPListen") && !json.get("UDPListen").isJsonNull()) {
current.setUDPListen(new MultiProtocolSocketAddress(json.get("UDPListen").getAsString()));
}
if (json.has("openConnections") || json.has("OpenConnections") || json.has("lineTable") || json.has("LineTable")) {
JsonArray arr = json.has("openConnections") ? json.getAsJsonArray("openConnections")
: json.has("OpenConnections") ? json.getAsJsonArray("OpenConnections")
: json.has("lineTable") ? json.getAsJsonArray("lineTable") : json.getAsJsonArray("LineTable");
List<MultiProtocolSocketAddress> list = new ArrayList<>();
for (JsonElement el : arr) {
if (el.isJsonPrimitive()) {
list.add(new MultiProtocolSocketAddress(el.getAsString()));
}
}
current.setOpenConnections(list);
}
if (json.has("autoConnections") || json.has("AutoConnections") || json.has("connectLineTable") || json.has("ConnectLineTable")) {
JsonArray arr = json.has("autoConnections") ? json.getAsJsonArray("autoConnections")
: json.has("AutoConnections") ? json.getAsJsonArray("AutoConnections")
: json.has("connectLineTable") ? json.getAsJsonArray("connectLineTable") : json.getAsJsonArray("ConnectLineTable");
List<MultiProtocolSocketAddress> list = new ArrayList<>();
for (JsonElement el : arr) {
if (el.isJsonPrimitive()) {
list.add(new MultiProtocolSocketAddress(el.getAsString()));
}
}
current.setAutoConnections(list);
}
if (json.has("ntpServers") || json.has("NtpServers") || json.has("ntpServerTable")) {
JsonArray arr = json.has("ntpServers") ? json.getAsJsonArray("ntpServers")
: json.has("NtpServers") ? json.getAsJsonArray("NtpServers")
: json.getAsJsonArray("ntpServerTable");
List<MultiProtocolSocketAddress> list = new ArrayList<>();
for (JsonElement el : arr) {
if (el.isJsonPrimitive()) {
list.add(new MultiProtocolSocketAddress(el.getAsString()));
}
}
current.setNtpServers(list);
}
if (json.has("dns") || json.has("DNS")) {
JsonArray arr = json.has("dns") ? json.getAsJsonArray("dns") : json.getAsJsonArray("DNS");
List<InetAddress> list = new ArrayList<>();
for (JsonElement el : arr) {
if (el.isJsonPrimitive()) {
try {
list.add(InetAddress.getByName(el.getAsString()));
} catch (Exception ignored) {}
}
}
current.setDNS(list);
}
if (json.has("extraRoutes") || json.has("ExtraRoutes")) {
JsonArray arr = json.has("extraRoutes") ? json.getAsJsonArray("extraRoutes") : json.getAsJsonArray("ExtraRoutes");
List<String> list = new ArrayList<>();
for (JsonElement el : arr) {
if (el.isJsonPrimitive()) {
list.add(el.getAsString());
}
}
current.setExtraRoutes(list);
}
if (json.has("networkInterfaceExcepts") || json.has("NetworkInterfaceExcepts")) {
JsonArray arr = json.has("networkInterfaceExcepts") ? json.getAsJsonArray("networkInterfaceExcepts") : json.getAsJsonArray("NetworkInterfaceExcepts");
List<String> list = new ArrayList<>();
for (JsonElement el : arr) {
if (el.isJsonPrimitive()) {
list.add(el.getAsString());
}
}
current.setNetworkInterfaceExcepts(list);
}
if (json.has("congestionAlgorithm") && !json.get("congestionAlgorithm").isJsonNull()) {
current.setCongestionAlgorithm(json.get("congestionAlgorithm").getAsString());
}
if (json.has("performanceStrategy") && !json.get("performanceStrategy").isJsonNull()) {
current.setPerformanceStrategy(json.get("performanceStrategy").getAsString());
}
if (json.has("linkConnectionsCount")) {
current.setLinkConnectionsCount(json.get("linkConnectionsCount").getAsInt());
}
if (json.has("burstLimit")) {
current.setBurstLimit(json.get("burstLimit").getAsDouble());
}
if (json.has("delayUpperBound")) {
current.setDelayUpperBound(json.get("delayUpperBound").getAsDouble());
}
if (json.has("delayLowerBound")) {
current.setDelayLowerBound(json.get("delayLowerBound").getAsDouble());
}
if (json.has("nagleDelayTime")) {
current.setNagleDelayTime(json.get("nagleDelayTime").getAsLong());
}
if (json.has("linkNagleDelayTime")) {
current.setLinkNagleDelayTime(json.get("linkNagleDelayTime").getAsLong());
}
if (json.has("denyConnectionQuery")) {
current.setDenyConnectionQuery(json.get("denyConnectionQuery").getAsBoolean());
} else if (json.has("denyLineTableQuery")) {
current.setDenyConnectionQuery(json.get("denyLineTableQuery").getAsBoolean());
}
if (json.has("denyConnectionBroadcast")) {
current.setDenyConnectionBroadcast(json.get("denyConnectionBroadcast").getAsBoolean());
} else if (json.has("denyLineTableBroadcast")) {
current.setDenyConnectionBroadcast(json.get("denyLineTableBroadcast").getAsBoolean());
}
// Trigger GUI save consumer or save directly
if (proxySystem.getKLALBGUI() != null && proxySystem.getKLALBGUI().getSaveComsumer() != null) {
proxySystem.getKLALBGUI().getSaveComsumer().accept(proxySystem.getConfig());
} else {
proxySystem.saveConfigToFile();
}
JsonObject resp = new JsonObject();
resp.addProperty("success", true);
resp.addProperty("message", "Configuration updated successfully");
sendJsonResponse(exchange, 200, resp);
} else {
sendError(exchange, 500, "Current configuration is null");
}
} catch (Exception e) {
sendError(exchange, 400, "Failed to update configuration: " + e.getMessage());
}
} else {
sendError(exchange, 405, "Method not allowed");
}
}
// -------------------------------------------------------------
// SSE Handler & Broadcaster
// -------------------------------------------------------------
private void handleEvents(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
sendError(exchange, 405, "Method not allowed");
return;
}
Headers headers = exchange.getResponseHeaders();
headers.set("Content-Type", "text/event-stream; charset=utf-8");
headers.set("Cache-Control", "no-cache, no-transform");
headers.set("Connection", "keep-alive");
headers.set("Access-Control-Allow-Origin", "*");
exchange.sendResponseHeaders(200, 0);
sseClients.add(exchange);
}
private void startSseBroadcaster() {
sseExecutor.scheduleAtFixedRate(() -> {
if (sseClients.isEmpty()) return;
JsonObject eventData = new JsonObject();
eventData.add("status", getStatusJson());
eventData.add("links", getLinksJson());
String eventStr = "data: " + eventData.toString() + "\n\n";
byte[] bytes = eventStr.getBytes(StandardCharsets.UTF_8);
Iterator<HttpExchange> it = sseClients.iterator();
while (it.hasNext()) {
HttpExchange ex = it.next();
try {
OutputStream os = ex.getResponseBody();
os.write(bytes);
os.flush();
} catch (Exception e) {
try {
ex.close();
} catch (Exception ignored) {}
it.remove();
}
}
}, 200, 200, TimeUnit.MILLISECONDS);
}
// -------------------------------------------------------------
// Static Resource / SPA Handler
// -------------------------------------------------------------
private void handleStatic(HttpExchange exchange) throws IOException {
if (handleCorsPreflight(exchange)) return;
String path = exchange.getRequestURI().getPath();
if (path.startsWith("/api/")) {
sendError(exchange, 404, "API endpoint not found");
return;
}
// Look for dashboard/dist
File distDir = new File("dashboard/dist");
File targetFile = null;
if (distDir.exists() && distDir.isDirectory()) {
String relPath = path.equals("/") ? "index.html" : path.substring(1);
targetFile = new File(distDir, relPath);
if (!targetFile.exists() || targetFile.isDirectory()) {
targetFile = new File(distDir, "index.html");
}
}
if (targetFile != null && targetFile.exists() && targetFile.isFile()) {
String mime = getMimeType(targetFile.getName());
byte[] content = Files.readAllBytes(targetFile.toPath());
setCorsHeaders(exchange);
exchange.getResponseHeaders().set("Content-Type", mime);
exchange.sendResponseHeaders(200, content.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(content);
}
} else {
// Fallback welcome message if frontend not built yet
String html = "<!DOCTYPE html><html><head><meta charset='utf-8'><title>KLALB Web Dashboard</title>" +
"<style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#09090b;color:#fafafa}" +
".card{background:#18181b;padding:2rem;border-radius:0.75rem;border:1px solid #27272a;max-width:480px;text-align:center}" +
"h1{margin:0 0 0.5rem;font-size:1.5rem}p{color:#a1a1aa;margin:0 0 1rem;font-size:0.875rem}code{background:#27272a;padding:0.2rem 0.4rem;border-radius:0.25rem}</style></head>" +
"<body><div class='card'><h1>KLALB Web Dashboard API Active</h1>" +
"<p>The backend API is ready. Start the frontend dev server or build the dashboard: <br><code>cd dashboard && pnpm build</code></p>" +
"<p><a href='/api/status' style='color:#38bdf8'>View /api/status</a></p></div></body></html>";
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
setCorsHeaders(exchange);
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
}
private String getMimeType(String filename) {
String lower = filename.toLowerCase();
if (lower.endsWith(".html")) return "text/html; charset=utf-8";
if (lower.endsWith(".js") || lower.endsWith(".mjs")) return "application/javascript; charset=utf-8";
if (lower.endsWith(".css")) return "text/css; charset=utf-8";
if (lower.endsWith(".json")) return "application/json; charset=utf-8";
if (lower.endsWith(".svg")) return "image/svg+xml";
if (lower.endsWith(".png")) return "image/png";
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
if (lower.endsWith(".ico")) return "image/x-icon";
if (lower.endsWith(".woff2")) return "font/woff2";
if (lower.endsWith(".woff")) return "font/woff";
if (lower.endsWith(".ttf")) return "font/ttf";
return "application/octet-stream";
}
}
@@ -43,6 +43,10 @@ public class KLALBRoutingProtocol extends Thread{
private RouterInfo selfRouterInfo; private RouterInfo selfRouterInfo;
private Map<IPv6Address, RouterInfo> netmap=new ConcurrentHashMap<>(); private Map<IPv6Address, RouterInfo> netmap=new ConcurrentHashMap<>();
public Map<IPv6Address, RouterInfo> getNetmap() {
return netmap;
}
private volatile Map<IPv6Address,Long>addresses; private volatile Map<IPv6Address,Long>addresses;
@@ -22,7 +22,7 @@ public class KLALBRoutingProtocolAPIServer {
InetSocketAddress addrs=(InetSocketAddress) addr; InetSocketAddress addrs=(InetSocketAddress) addr;
switch(dataobj.getType()){ switch(dataobj.getType()){
case KLALBRoutingProtocolJsonData.OPEN_LINES_REQ: case KLALBRoutingProtocolJsonData.OPEN_LINES_REQ:
if(controller.getConfigItem()==null||(!controller.getConfigItem().isDenyLineTableQuery())) { if(controller.getConfigItem()==null||(!controller.getConfigItem().isDenyConnectionQuery())) {
KLALBRoutingProtocolJsonData json=new KLALBRoutingProtocolJsonData(KLALBRoutingProtocolJsonData.OPEN_LINES_RESP,dataobj.getUuid(),controller.getSelflineTable()); KLALBRoutingProtocolJsonData json=new KLALBRoutingProtocolJsonData(KLALBRoutingProtocolJsonData.OPEN_LINES_RESP,dataobj.getUuid(),controller.getSelflineTable());
routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(json),addr); routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(json),addr);
} }