From be82f5527736d6121f04357b459da281a595ab04 Mon Sep 17 00:00:00 2001 From: SerinaNya <34389622+SerinaNya@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:14:04 +0800 Subject: [PATCH 1/4] feat(srv6)!: add node-info update invalidation Publish node metadata through Tiny and Full queries, synchronize Swing and web topology details, and document the updated protocol workflow. BREAKING CHANGE: RouterInfo no longer carries device names; peers must use Tiny node-info queries. --- AGENTS.md | 118 ++----- dashboard | 2 +- src/klalb_en_US.properties | 1 + src/klalb_zh_CN.properties | 5 +- .../cloud/network/klalb/KLALBController.java | 202 ++++++++++-- .../network/klalb/ui/KLALBStateGUI3.java | 121 ++++--- .../network/klalb/ui/NetworkGraphPanel.java | 195 +++++++++++- .../klalb/ui/NodeInformationPanel.java | 112 ++++--- .../network/klalb/web/KLALBWebServer.java | 301 ++++++++++++++++-- .../cloud/network/srv6/JsonDataPacket.java | 12 +- .../network/srv6/KLALBNodeInformation.java | 18 +- .../network/srv6/KLALBRoutingProtocol.java | 170 +++++++--- .../srv6/KLALBRoutingProtocolAPIClient.java | 16 +- .../srv6/KLALBRoutingProtocolAPIServer.java | 35 +- .../srv6/KLALBRoutingProtocolJsonData.java | 38 ++- .../srv6/KLALBRoutingProtocolPacket.java | 34 +- .../kne/cloud/network/srv6/RouterInfo.java | 51 +-- .../cloud/network/srv6/RouterInfoPacket.java | 72 ++++- 18 files changed, 1130 insertions(+), 373 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 16377ee..7d962ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,101 +1,53 @@ -# AGENTS.md +# KLALB Repository Guide -KLALB ("KLALB Decentralized SRv6 Network") — Java load-balancing/tunnel system that merges multiple WAN links into one virtual IPv6/SRv6 network. Version constant lives in `src/org/kne/cloud/network/klalb/CONST.java`. Protocol specs and manuals are the Chinese `.docx` files in the repo root. +KLALB is a Java SRv6/load-balancing system. The executable entrypoint is `org.kne.cloud.network.klalb.KLALBMain`; runtime configuration is `klalb-config.json` in the repository root. -## Build & run +## Build And Run -No Maven/Gradle. Plain Eclipse/IntelliJ project: dependencies are vendored jars in `lib/`, output goes to `bin/` (gitignored). When adding a jar, update **both** `.classpath` and `KLALB.iml`. - -Compile (`javac` is NOT on PATH — use the full JDK path; `-encoding UTF-8` is mandatory because sources contain Chinese text): +- This is a plain Eclipse/IntelliJ Java project: sources are `src/`, vendored dependencies are `lib/`, and output is `bin/`. There is no Maven or Gradle. +- `.classpath` targets `JavaSE-25`. Sources contain Chinese text, so manual compilation must use UTF-8: ```powershell & "C:\Program Files\Zulu\zulu-25\bin\javac.exe" -encoding UTF-8 -cp "lib/*" -d bin (Get-ChildItem -Recurse src -Filter *.java | ForEach-Object FullName) ``` -Warnings about `ThreadTool` varargs / deprecated `finalize` are pre-existing and expected — success = exit code 0. After recompiling, restart the running app (IDE-debugged JVMs keep old classes). - -Run from the repo root — CWD matters: -- reads `klalb-config.json` from CWD -- loads native libs from CWD: `tuntap4j.dll/.so/.dylib`, `wintun.dll`, `fastcopy.dll` (TUN device support) -- classpath must include `src` as well as `bin`: i18n bundles (`/klalb_*.properties`) and images (`/assets/*`) are classpath resources that Eclipse copies to `bin` but manual `javac` does not +- Run from the repository root. Manual `javac` does not copy resources, so keep `src` on the runtime classpath: ```powershell -java --enable-native-access=ALL-UNNAMED "--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED" -cp "bin;src;lib/*" org.kne.cloud.network.klalb.KLALBMain +& "C:\Program Files\Zulu\zulu-25\bin\java.exe" --enable-native-access=ALL-UNNAMED "--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED" -cp "bin;src;lib/*" org.kne.cloud.network.klalb.KLALBMain ``` -IDE metadata targets JDK 26 (`jdk-26.0.1`); the tree also compiles cleanly on JDK 25. `.classpath` now references the standard container `JavaSE-25` — an execution-environment spec that any JDK ≥25 satisfies, so it works unchanged on JDK 26 machines too (the original named `jdk-26.0.1` VM broke VS Code import on machines without it). `.vscode/settings.json` maps `JavaSE-25` to the locally installed Adoptium JDK; register every installed JDK there when adding another one. Keep compiler compliance ≤25 (`.settings` pins 19) so both JDKs stay usable. +- The native libraries and `klalb-config.json` are resolved from the current directory. Restart a running JVM after recompiling. +- TUN creation normally needs elevation. For non-admin UI/routing checks, set `"enableTUN": false`. +- Current full compilation emits 11 pre-existing varargs/deprecation warnings; exit code `0` is success. -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). -- 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 `"enableTUN": false` in `klalb-config.json` or toggle off "启用 TUN 虚拟网卡" in GUI/Web settings. -- Routing broadcast (`RouterInfo`) transmits `deviceName`, which topology and node overview panels display. `deviceDescription` is NOT broadcast — it only leaves the node in full node-info query responses (see srv6 API below); `ExtraRoutes` remain local controller configs. +## Dashboard + +- `dashboard/` is a Git submodule. Commit dashboard changes inside it, then update the parent repository's submodule pointer. +- Run frontend commands from `dashboard/` with pnpm: + +```powershell +pnpm install --frozen-lockfile +pnpm lint +pnpm typecheck +pnpm build +pnpm dev +``` + +- `pnpm build` runs `tsc -b` then Vite and writes `dashboard/dist`, which the Java web server hosts. Vite development proxies `/api` to `http://127.0.0.1:4665`. +- Add shadcn components through `pnpm dlx shadcn@latest add `; do not hand-create replacements for installed shadcn primitives. ## Verification -No test suite, no CI. Classes named `*Test*` (`nathole/`, `ntp/`) are manual `main()` harnesses requiring real network peers. Practical check = compile succeeds + app launches. +- There is no CI or automated test suite. `*Test*` classes are manual harnesses that require real network peers. +- For Java changes, compile and launch the app. For dashboard changes, run `pnpm typecheck` and `pnpm build`. -## Architecture +## Important Boundaries -- 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, default port `4665`) → 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/routing-table`, `/api/nodes` (topology graph), `/api/node-info?address=` (on-demand full node info), `/api/config`. - - Static file hosting / SPA fallback: serves `dashboard/dist/` assets directly. -- `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.congestion` — pluggable congestion control (BBR, Vegas2, DCTCP...), chosen via `"congestionAlgorithm"` in config. -- `...network.kltp` — custom reliable transport protocol (packets/streams). -- `...network.ipv6`, `...network.srv6` — packet codecs, route table, Dijkstra path computation. -- **Node-info query API** (`...network.srv6`, JSON datagrams on `KLALBRoutingProtocol.DEFAULT_PORT=1001`): `KLALBRoutingProtocolAPIServer/Client` speak two request types — - - `nodeinfotinyreq/resp` → device name ONLY; never gated by any flag (name is public via broadcast anyway). - - `nodeinfofullreq/resp` → externalEndpoints + deviceName + deviceDescription. `denyExternalEndpointQuery=true` hides ONLY the endpoint list (`data=null`); name/description still answer. - - GUI rule: opening `NodeInformationPanel` = Full query; use `requestNodeInfoTiny` for lightweight/background lookups. Legacy `openlines*` message types were removed — mixed-version meshes get silence, so upgrade the whole network together. - - `JsonDataPacket` stores its UTF-8 payload length in a 2-byte header field: keep every JSON message under 64 KiB. -- `...network.frpc` — frp client integration. -- `...klalb.ui` — all Swing UI code. - -## Frontend (Dashboard) - -Located in `dashboard/`: -- **Git layout**: `dashboard/` is a separate git repo wired in as a submodule (own origin on `git.code.cq.cn`). Commit frontend changes inside `dashboard/` first, then bump the submodule pointer in the parent repo — parent-repo commits alone do not capture them. -- **Stack**: Vite + React 19 + TypeScript + Tailwind CSS v4 + `@base-ui/react` (style: `base-nova`, icons: `lucide-react`, toasts: `@base-ui/react/toast`). -- **Routing**: Hash-based routing (`#/overview`, `#/connections`, `#/routing-table`, `#/topology`, `#/settings`) for seamless SPA hosting under Java `KLALBWebServer`. -- **Package Manager**: `pnpm` (run all commands from `dashboard/` directory). -- **Component installation**: **Must** use CLI via `pnpm dlx shadcn@latest add ` (e.g. `pnpm dlx shadcn@latest add alert card badge toast`). Never create or fake shadcn components manually. Non-shadcn libs (`@xyflow/react`, `d3-force`) are installed via plain `pnpm add`. -- **Commands**: - - `pnpm dev` — Start Vite dev server (proxies `/api` to backend `http://127.0.0.1:4665`). - - `pnpm build` — Typecheck and build SPA to `dashboard/dist` (which Java `KLALBWebServer` serves directly). - - `pnpm lint` / `pnpm typecheck` — Verification. -- **Pages & data flow**: - - Overview / Connections read the SSE stream (`use-klalb-sse.ts`, 200ms pushes of status + links). - - Settings loads/saves `/api/config` (`use-klalb-config.ts`); save payload must keep legacy field aliases alongside new names for compatibility. - - Routing table polls `/api/routing-table` every 1s (`use-routing-table.ts`); `cost` is delay-derived and displayed in milliseconds. - - Topology polls `/api/nodes` every 1s (`use-topology.ts`) — SSE does NOT carry topology. - - Selecting a topology node queries `/api/node-info`; remote node descriptions require a full SRv6 node-info request and can time out after 3s. - - Topology layout: `d3-force` headless simulation (recomputed only when node/edge structure changes) rendered by `@xyflow/react` with custom `device-node` / `link-edge` components in `src/components/topology/`. - - React hooks lint rule forbids `setState` synchronously inside effects — initialize form state via component `key` remount + lazy `useState(() => ...)` initializers (see `SettingsForm` pattern). - -## 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`). - -Key controller config fields: -- `externalEndpoints` / `autoConnections`: published vs auto-connect endpoint lists (renamed from `openConnections`, which itself replaced legacy `LineTable`; the old name was a developer naming mistake — these addresses are this node's externally published endpoints, not "connections"). -- `ntpServers`: time server list (replaces `ntpServerTable`). -- `denyExternalEndpointQuery` / `denyExternalEndpointBroadcast`: safety flags — the query flag hides ONLY the external-endpoint list in full node-info responses (device name/description still answer; Tiny queries are never gated), the broadcast flag disables LAN multicast discovery (renamed from `denyConnectionQuery` / `denyConnectionBroadcast`, which replaced `denyLineTableQuery` / `denyLineTableBroadcast`). -- `enableTUN`: boolean flag for TUN interface creation (`"TUNName"` configures device name). -- `webListen`: Web API listen address, normally `http://0.0.0.0:4665`; legacy `webPort` is accepted on load/API input. - -Legacy JSON keys are still accepted on load: `KLALBConfigItem.getDefaultJsonDeserializer()` normalizes old key names (`openConnections`/`LineTable`, `denyConnectionQuery`, `denyLineTable*`, ...) before reflective deserialization (manual rewrite because gson-2.1 has no `@SerializedName(alternate=...)`), and `handleConfig` in the web server accepts them too. New saves always write canonical names. - -Gson quirks: -- **gson-2.1 (vendored) is ancient**: its `JSON_ELEMENT` adapter factory only matches exact `JsonElement.class`, NOT subclasses. Calling `gson.toJson(Object)` with a runtime `JsonObject`/`JsonArray` reflectively serializes the internal field as `{"members": {...}}`. `KLALBWebServer.sendJsonResponse` guards against this by using `JsonElement.toString()` for JsonElement instances — keep that guard when adding new response paths. SSE avoids the issue entirely via `JsonObject.toString()`. -- `/api/config` GET/POST is parsed field-by-field in `KLALBWebServer.handleConfig` (NOT whole-object Gson reflection) because polymorphic fields (`List`, `List`) break reflective mapping. Keep new config fields in sync there, accepting both legacy and new JSON key names. -- `InetAddress`, `MultiProtocolSocketAddress`, and `KLALBConfigItem` custom adapters are registered on the shared Gson in `KLALBProxySystem`; the web server reuses that instance via `proxySystem.getGson()`. -- Saving via web API persists through `KLALBProxySystem.saveConfigToFile()` (GUI save consumer takes precedence when present). - -## Conventions - -- Sources are UTF-8; comments, log/UI strings, and commit messages are largely Chinese. -- UI strings go through `UIEnv.getRsb().getString(...)`; add keys to **both** `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`. -- `client.cfg`, `server.cfg`, `linetable.txt` at the root are example line-table/port-rule files loaded via the GUI file picker — not hardwired paths. +- `KLALBConfigItem` is a polymorphic JSON array keyed by `Type`. Adding a type requires a subclass and cases in both default config serializer and deserializer; unknown types must remain preserved. +- `/api/config` is field-by-field parsing, not whole-object Gson mapping. Keep legacy key aliases in sync with new fields. +- Vendored Gson is `2.1`: responses that are `JsonElement` instances must be serialized with `JsonElement.toString()`, not reflective `gson.toJson(Object)`. +- UI strings use `UIEnv.getRsb()`; add keys to both `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`. +- `KLALBController.PublishedNodeInfo` is the thread-safe source for Tiny/Full node-info responses. Publish name, description, external endpoints, and Extra Routes through the controller method so snapshots and Tiny/Full update flags stay consistent. +- `RouterInfo` no longer carries a device name. Its wire format retains an empty legacy UTF slot and `RouterInfoPacket` has optional Tiny/Full invalidation flags. Treat codec changes as compatibility work: preserve old-reader behavior and review a whole-mesh rollout. +- Full node-info carries `extraRoutes` separately from the endpoint `data` list. Keep absent fields compatible with older peers. diff --git a/dashboard b/dashboard index 89474b9..0b705f9 160000 --- a/dashboard +++ b/dashboard @@ -1 +1 @@ -Subproject commit 89474b9e91791b8efe098a13171e6ff962ad7ba1 +Subproject commit 0b705f9ba0008540099e307bbb1e3e6944d7aa71 diff --git a/src/klalb_en_US.properties b/src/klalb_en_US.properties index e2bf3d3..02a5e59 100644 --- a/src/klalb_en_US.properties +++ b/src/klalb_en_US.properties @@ -7,6 +7,7 @@ devicename=Device name devicedescription=Device description dnsserver=DNS server extraroutes=Extra routes +noextraroutes=No extra routes asnumber=AS number tcplistening=TCP listening udplistening=UDP listening diff --git a/src/klalb_zh_CN.properties b/src/klalb_zh_CN.properties index 9c46648..fc1b624 100644 --- a/src/klalb_zh_CN.properties +++ b/src/klalb_zh_CN.properties @@ -6,7 +6,8 @@ ipv6addr=IPv6地址 devicename=设备名称 devicedescription=设备描述 dnsserver=DNS服务器 -extraroutes=额外路由 +extraroutes=额外路由 +noextraroutes=暂无额外路由 asnumber=AS号码 tcplistening=TCP监听端口 udplistening=UDP监听端口 @@ -116,4 +117,4 @@ webapisettings=Web API 设置 enablewebapi=启用 Web API weblistenaddr=Web API 监听地址:端口 invaildweblistenaddr=无效的 Web API 监听地址:端口 -enabletun=启用TUN虚拟网卡 \ No newline at end of file +enabletun=启用TUN虚拟网卡 diff --git a/src/org/kne/cloud/network/klalb/KLALBController.java b/src/org/kne/cloud/network/klalb/KLALBController.java index 432fca2..ebbbc61 100644 --- a/src/org/kne/cloud/network/klalb/KLALBController.java +++ b/src/org/kne/cloud/network/klalb/KLALBController.java @@ -10,12 +10,14 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Enumeration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.Timer; import java.util.TimerTask; @@ -76,7 +78,50 @@ public class KLALBController { new HashMapTimestampMonitor(HighAccuracyClock.SYSTEM_CLOCK, "up", 100, TIME_WINDOW), new HashMapTimestampMonitor(HighAccuracyClock.SYSTEM_CLOCK, "down", 100, TIME_WINDOW)); - private List externalEndpoints = new ArrayList<>(); + private List externalEndpoints = new ArrayList<>(); + private List configuredExternalEndpoints = new ArrayList<>(); + private List discoveredExternalEndpoints = new ArrayList<>(); + + public static final class PublishedNodeInfo { + private final String deviceName; + private final String deviceDescription; + private final List externalEndpoints; + private final List extraRoutes; + private final long fullRevision; + + private PublishedNodeInfo(String deviceName, String deviceDescription, + List externalEndpoints, List extraRoutes, long fullRevision) { + this.deviceName = deviceName; + this.deviceDescription = deviceDescription; + this.externalEndpoints = Collections.unmodifiableList( + new ArrayList(externalEndpoints)); + this.extraRoutes = Collections.unmodifiableList(new ArrayList(extraRoutes)); + this.fullRevision = fullRevision; + } + + public String getDeviceName() { + return deviceName; + } + + public String getDeviceDescription() { + return deviceDescription; + } + + public List getExternalEndpoints() { + return externalEndpoints; + } + + public List getExtraRoutes() { + return extraRoutes; + } + + public long getFullRevision() { + return fullRevision; + } + } + + private volatile PublishedNodeInfo publishedNodeInfo = new PublishedNodeInfo(null, null, + Collections.emptyList(), Collections.emptyList(), 0L); private List listensSocketAddress = new CopyOnWriteArrayList<>(); @@ -125,7 +170,8 @@ public class KLALBController { @Override public void run() { try { - List localaddress = networkInterfaceManager.getAllNetworkInterfaceAddress(); + List localaddress = networkInterfaceManager.getAllNetworkInterfaceAddress(); + List currentDiscoveredExternalEndpoints = new ArrayList<>(); for (InetAddress inetAddress : localaddress) { for (Iterator iterator = listensSocketAddress.iterator(); iterator.hasNext();) { @@ -137,11 +183,9 @@ public class KLALBController { MultiProtocolSocketAddress bind = new MultiProtocolSocketAddress(tcpl.getProtocol(), inetAddress.getHostAddress(), tcpl.getPort()); // System.out.println(bind); - synchronized (externalEndpoints) { - if (!externalEndpoints.contains(bind)) { - externalEndpoints.add(bind); - } - } + if (!currentDiscoveredExternalEndpoints.contains(bind)) { + currentDiscoveredExternalEndpoints.add(bind); + } } } catch (UnknownHostException e) { // TODO 自动生成的 catch 块 @@ -149,9 +193,15 @@ public class KLALBController { } } - } - - lineslock.writeLock().lock(); + } + synchronized (externalEndpoints) { + if (!discoveredExternalEndpoints.equals(currentDiscoveredExternalEndpoints)) { + discoveredExternalEndpoints = currentDiscoveredExternalEndpoints; + publishDiscoveredExternalEndpointLocked(); + } + } + + lineslock.writeLock().lock(); try { @@ -298,8 +348,8 @@ public class KLALBController { private boolean checkIsSelf(MultiProtocolSocketAddress inetAddress) throws UnknownHostException { - return inetAddress.getInetAddress().isAnyLocalAddress() || inetAddress.getInetAddress().isLoopbackAddress() - || externalEndpoints.contains(inetAddress); + return inetAddress.getInetAddress().isAnyLocalAddress() || inetAddress.getInetAddress().isLoopbackAddress() + || publishedNodeInfo.getExternalEndpoints().contains(inetAddress); } private boolean checkIsSelfLocator(InetAddress inetAddress) { @@ -401,9 +451,104 @@ public class KLALBController { } - public List getExternalEndpoints() { - return externalEndpoints; - } + public List getExternalEndpoints() { + return publishedNodeInfo.getExternalEndpoints(); + } + + public List getExternalEndpointsSnapshot() { + return new ArrayList(publishedNodeInfo.getExternalEndpoints()); + } + + public PublishedNodeInfo getPublishedNodeInfo() { + return publishedNodeInfo; + } + + private List createEffectiveExternalEndpointsLocked() { + List effective = new ArrayList(); + for (MultiProtocolSocketAddress endpoint : configuredExternalEndpoints) { + if (!effective.contains(endpoint)) { + effective.add(endpoint); + } + } + for (MultiProtocolSocketAddress endpoint : discoveredExternalEndpoints) { + if (!effective.contains(endpoint)) { + effective.add(endpoint); + } + } + return effective; + } + + private void publishNodeInfoLocked(String deviceName, String deviceDescription, + List configEndpoints, List extraRoutes) { + configuredExternalEndpoints = configEndpoints == null + ? new ArrayList() + : new ArrayList(configEndpoints); + List publishedExtraRoutes = extraRoutes == null + ? new ArrayList() : new ArrayList(extraRoutes); + List effective = createEffectiveExternalEndpointsLocked(); + PublishedNodeInfo previous = publishedNodeInfo; + boolean nameChanged = !Objects.equals(previous.getDeviceName(), deviceName); + boolean fullChanged = !Objects.equals(previous.getDeviceDescription(), deviceDescription) + || !previous.getExternalEndpoints().equals(effective) + || !previous.getExtraRoutes().equals(publishedExtraRoutes); + + externalEndpoints.clear(); + externalEndpoints.addAll(effective); + if (srv6Router != null) { + srv6Router.setDeviceName(deviceName); + } + long fullRevision = previous.getFullRevision() + (fullChanged ? 1L : 0L); + publishedNodeInfo = new PublishedNodeInfo(deviceName, deviceDescription, effective, publishedExtraRoutes, + fullRevision); + if (routingProtocol != null) { + if (nameChanged) { + routingProtocol.announceNodeInfoUpdate( + org.kne.cloud.network.srv6.RouterInfoPacket.NODE_INFO_TINY_UPDATE_REQUIRED); + } + if (fullChanged) { + routingProtocol.announceNodeInfoUpdate( + org.kne.cloud.network.srv6.RouterInfoPacket.NODE_INFO_FULL_UPDATE_REQUIRED); + } + } + } + + public void publishNodeInfo(String deviceName, String deviceDescription, + List configEndpoints, List extraRoutes) { + synchronized (externalEndpoints) { + if (configItem != null) { + configItem.setDeviceName(deviceName); + configItem.setDeviceDescription(deviceDescription); + configItem.setExternalEndpoints(new ArrayList(configEndpoints == null + ? Collections.emptyList() : configEndpoints)); + configItem.setExtraRoutes(new ArrayList(extraRoutes == null + ? Collections.emptyList() : extraRoutes)); + } + publishNodeInfoLocked(deviceName, deviceDescription, configEndpoints, extraRoutes); + } + } + + private void publishDiscoveredExternalEndpointLocked() { + List effective = createEffectiveExternalEndpointsLocked(); + if (new HashSet(effective) + .equals(new HashSet(externalEndpoints))) { + return; + } + externalEndpoints.clear(); + externalEndpoints.addAll(effective); + PublishedNodeInfo previous = publishedNodeInfo; + publishedNodeInfo = new PublishedNodeInfo(previous.getDeviceName(), previous.getDeviceDescription(), effective, + previous.getExtraRoutes(), previous.getFullRevision() + 1L); + if (routingProtocol != null) { + routingProtocol.announceNodeInfoUpdate( + org.kne.cloud.network.srv6.RouterInfoPacket.NODE_INFO_FULL_UPDATE_REQUIRED); + } + } + + public void replaceExternalEndpoints(List endpoints) { + PublishedNodeInfo published = publishedNodeInfo; + publishNodeInfo(published.getDeviceName(), published.getDeviceDescription(), endpoints, + published.getExtraRoutes()); + } public IPv6AddressGroup getSelf() { @@ -602,10 +747,9 @@ public class KLALBController { } } - private String generateExternalEndpointsString() { - StringBuilder sbd = new StringBuilder(); - for (Iterator iterator = externalEndpoints.iterator(); iterator.hasNext();) { - MultiProtocolSocketAddress klalbRemoteLine = (MultiProtocolSocketAddress) iterator.next(); + private String generateExternalEndpointsString() { + StringBuilder sbd = new StringBuilder(); + for (MultiProtocolSocketAddress klalbRemoteLine : publishedNodeInfo.getExternalEndpoints()) { sbd.append(klalbRemoteLine.toString()); sbd.append('\n'); } @@ -764,8 +908,11 @@ public class KLALBController { } } }).start(); - loadSRv6ProtocolStack(selfg, enableVirtualAdapter); - loadController(); + loadSRv6ProtocolStack(selfg, enableVirtualAdapter); + if (configItem == null) { + publishNodeInfo(srv6Router.getDeviceName(), null, null, null); + } + loadController(); } public KLALBController(boolean enableVirtualAdapter, List dnsaddr) { @@ -801,10 +948,9 @@ public class KLALBController { getIpv6Router().setASN(vasn); } - List linele = configItem.getExternalEndpoints(); - if (linele != null) { - getExternalEndpoints().addAll(linele); - } + List linele = configItem.getExternalEndpoints(); + publishNodeInfo(configItem.getDeviceName(), configItem.getDeviceDescription(), linele, + configItem.getExtraRoutes()); List linetoc = configItem.getAutoConnections(); if (linetoc != null) { linetoc.forEach((aline) -> { diff --git a/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java b/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java index 45735cb..b110b94 100644 --- a/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java +++ b/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java @@ -5,9 +5,10 @@ import java.awt.datatransfer.StringSelection; import java.awt.event.*; import java.io.IOException; import java.net.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Timer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Timer; import java.util.TimerTask; import java.util.function.Consumer; import javax.imageio.ImageIO; @@ -26,7 +27,7 @@ import org.kne.cloud.network.ipv6.IPv6NetworkLink; import org.kne.cloud.network.klalb.*; import org.kne.cloud.network.klalb.ui.GraphPanel.GraphNode; import org.kne.cloud.network.monitor.LinkStatus; -import org.kne.cloud.network.srv6.SRv6Router; +import org.kne.cloud.network.srv6.SRv6Router; import org.kne.ui.XFrame; import org.kne.ui.YScrollPane; @@ -1061,8 +1062,19 @@ public class KLALBStateGUI3 extends XFrame { } for (KLALBConfigItem item : config) { - if (item instanceof KLALBControllerConfigItem) { - KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item; + if (item instanceof KLALBControllerConfigItem) { + KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item; + String oldDeviceName = kck.getDeviceName(); + String oldDeviceDescription = kck.getDeviceDescription(); + List oldExternalEndpoints = kck.getExternalEndpoints() == null + ? null + : new ArrayList(kck.getExternalEndpoints()); + List oldExtraRoutes = kck.getExtraRoutes() == null + ? null : new ArrayList(kck.getExtraRoutes()); + String newDeviceName; + String newDeviceDescription; + List newExtraRoutes; + List newExternalEndpoints; // 保存语言设置 kck.setLanguage(((Language) comboLang.getSelectedItem()).name()); @@ -1070,23 +1082,19 @@ public class KLALBStateGUI3 extends XFrame { kck.setNogui(nogui.isSelected()); // 保存设备名称 - String dnametext = deviceNameSet.getText().trim(); - if (dnametext.equals("")) { - kck.setDeviceName(null); - } else { - kck.setDeviceName(dnametext); - } - if (kcontroller != null && kcontroller.getIpv6Router() != null) { - kcontroller.getIpv6Router().setDeviceName(kck.getDeviceName()); - } - - // 保存设备描述 - String ddesctext = deviceDescriptionSet.getText().trim(); - if (ddesctext.equals("")) { - kck.setDeviceDescription(null); - } else { - kck.setDeviceDescription(ddesctext); - } + String dnametext = deviceNameSet.getText().trim(); + if (dnametext.equals("")) { + newDeviceName = null; + } else { + newDeviceName = dnametext; + } + // 保存设备描述 + String ddesctext = deviceDescriptionSet.getText().trim(); + if (ddesctext.equals("")) { + newDeviceDescription = null; + } else { + newDeviceDescription = ddesctext; + } // 保存IPv6地址 @@ -1141,7 +1149,7 @@ public class KLALBStateGUI3 extends XFrame { } } } - kck.setExtraRoutes(eroutes); + newExtraRoutes = eroutes; // 保存ASN String asntext = asnFieldSet.getText(); @@ -1232,7 +1240,7 @@ public class KLALBStateGUI3 extends XFrame { } } } - kck.setExternalEndpoints(iaddr1); + newExternalEndpoints = iaddr1; // 保存自动连接线路表 String[] splt11 = connectLineTabelSet.getText().split("\n"); @@ -1304,7 +1312,23 @@ public class KLALBStateGUI3 extends XFrame { kck.setNagleDelayTime(nagleDelayTime.getSlider().getValue()*100000L); - kck.setLinkNagleDelayTime(linkNagleDelayTime.getSlider().getValue()*100000L); + kck.setLinkNagleDelayTime(linkNagleDelayTime.getSlider().getValue()*100000L); + + boolean deviceNameChanged = !Objects.equals(oldDeviceName, newDeviceName); + boolean deviceDescriptionChanged = !Objects.equals(oldDeviceDescription, newDeviceDescription); + boolean externalEndpointsChanged = !Objects.equals(oldExternalEndpoints, newExternalEndpoints); + boolean extraRoutesChanged = !Objects.equals(oldExtraRoutes, newExtraRoutes); + if (deviceNameChanged || deviceDescriptionChanged || externalEndpointsChanged || extraRoutesChanged) { + if (kcontroller != null) { + kcontroller.publishNodeInfo(newDeviceName, newDeviceDescription, newExternalEndpoints, + newExtraRoutes); + } else { + kck.setDeviceName(newDeviceName); + kck.setDeviceDescription(newDeviceDescription); + kck.setExternalEndpoints(newExternalEndpoints); + kck.setExtraRoutes(newExtraRoutes); + } + } } } @@ -1403,20 +1427,22 @@ public class KLALBStateGUI3 extends XFrame { if (tsk5 != null) { tsk5.cancel(); } - tsk5 = new TimerTask() { - @Override - public void run() { - if (isVisible()) { - graph.loadNodes(); - for (int i = 0; i < 100; i++) { - graph.runPhy(); - } - graph.repaint(); - graph.revalidate(); - } - } - }; - t.scheduleAtFixedRate(tsk5, 1000, 1000); + tsk5 = new TimerTask() { + @Override + public void run() { + SwingUtilities.invokeLater(() -> { + if (isVisible()) { + graph.loadNodes(); + for (int i = 0; i < 100; i++) { + graph.runPhy(); + } + graph.repaint(); + graph.revalidate(); + } + }); + } + }; + t.scheduleAtFixedRate(tsk5, 0, 1000); } /** @@ -1761,9 +1787,18 @@ public class KLALBStateGUI3 extends XFrame { /** * 关闭窗口并清理资源 */ - public void close() { - setVisible(false); - if (tsk != null) { + public void close() { + for (int i = 0; i < tabbedPane.getTabCount(); i++) { + Component component = tabbedPane.getComponentAt(i); + if (component instanceof NodeInformationPanel) { + ((NodeInformationPanel) component).close(); + } + } + if (graph != null) { + graph.close(); + } + setVisible(false); + if (tsk != null) { tsk.cancel(); } if (st != null) { diff --git a/src/org/kne/cloud/network/klalb/ui/NetworkGraphPanel.java b/src/org/kne/cloud/network/klalb/ui/NetworkGraphPanel.java index cd8e656..eb832cb 100644 --- a/src/org/kne/cloud/network/klalb/ui/NetworkGraphPanel.java +++ b/src/org/kne/cloud/network/klalb/ui/NetworkGraphPanel.java @@ -8,28 +8,58 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.BiConsumer; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; import org.kne.cloud.network.MultiProtocolSocketAddress; import org.kne.cloud.network.ipv6.IPv6Address; import org.kne.cloud.network.klalb.KLALBController; -import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection; +import org.kne.cloud.network.srv6.KLALBNodeInformation; +import org.kne.cloud.network.srv6.KLALBRoutingProtocol; +import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; +import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection; +import org.kne.cloud.network.srv6.RouterInfoPacket; public class NetworkGraphPanel extends GraphPanel { - private KLALBController controller; - private KLALBStateGUI3 klbgui; - + private KLALBController controller; + private KLALBStateGUI3 klbgui; + private final KLALBRoutingProtocolAPIClient nodeInfoClient; + private final KLALBRoutingProtocol nodeInfoRoutingProtocol; + private final BiConsumer nodeInfoUpdateListener; + private final Object nodeInfoLock = new Object(); + private final Map nodeNameCache = new HashMap(); + private final Map nodeNameCacheTimes = new HashMap(); + private final Map nodeInfoRequestTimes = new HashMap(); + private final Map nodeInfoRequestGenerations = new HashMap(); + private static final long NODE_INFO_REQUEST_TIMEOUT = 3000L; + private static final long NODE_INFO_CACHE_TTL = 60000L; + private long nextNodeInfoRequestGeneration; + private volatile boolean nodeInfoClosed; + public NetworkGraphPanel(KLALBController controller,KLALBStateGUI3 klbgui) { super(); this.controller = controller; this.klbgui=klbgui; + this.nodeInfoRoutingProtocol = controller.getIpv6Router().getKlalbRouteProtol(); + this.nodeInfoClient = new KLALBRoutingProtocolAPIClient(nodeInfoRoutingProtocol); + this.nodeInfoUpdateListener = (address, flags) -> { + if ((flags & RouterInfoPacket.NODE_INFO_TINY_UPDATE_REQUIRED) != 0) { + SwingUtilities.invokeLater(() -> invalidateTinyNodeInfo(address)); + } + }; + nodeInfoRoutingProtocol.addNodeInfoUpdateListener(nodeInfoUpdateListener); + cacheLocalNodeName(); } public NetworkGraphPanel(KLALBController kc) { @@ -153,17 +183,145 @@ public class NetworkGraphPanel extends GraphPanel { } /** - * 节点标签:显示广播得知的设备名称(若有)+IP地址 + * 节点标签:显示 Tiny API 查询的设备名称(若有)+IP地址 */ - private String getNodeText(IPv6Address address) { - StringBuilder sb=new StringBuilder(); - String dname=controller.getIpv6Router().getKlalbRouteProtol().getDeviceName(address); - if(dname!=null) - sb.append(dname).append('\n'); - sb.append(InetGraphNode.getText(address)); - return sb.toString(); - } + private String getNodeText(IPv6Address address) { + StringBuilder sb=new StringBuilder(); + String dname; + synchronized (nodeInfoLock) { + dname = nodeNameCache.get(address); + } + if(dname!=null&&!dname.isEmpty()) + sb.append(dname).append('\n'); + sb.append(InetGraphNode.getText(address)); + return sb.toString(); + } + private void cacheLocalNodeName() { + IPv6Address localAddress = controller.getIpv6Router().getLocator().getAddress(); + synchronized (nodeInfoLock) { + nodeNameCache.put(localAddress, controller.getIpv6Router().getDeviceName()); + nodeNameCacheTimes.put(localAddress, System.currentTimeMillis()); + } + } + + private void requestNodeInfoTiny(final IPv6Address address) { + final long requestTime = System.currentTimeMillis(); + final long requestGeneration; + synchronized (nodeInfoLock) { + if (nodeInfoClosed) + return; + Long cachedAt = nodeNameCacheTimes.get(address); + if (nodeNameCache.containsKey(address) && cachedAt != null + && requestTime - cachedAt < NODE_INFO_CACHE_TTL) + return; + nodeNameCache.remove(address); + nodeNameCacheTimes.remove(address); + Long previousRequestTime = nodeInfoRequestTimes.get(address); + if (previousRequestTime != null && requestTime - previousRequestTime < NODE_INFO_REQUEST_TIMEOUT) + return; + nodeInfoRequestTimes.put(address, requestTime); + requestGeneration = ++nextNodeInfoRequestGeneration; + nodeInfoRequestGenerations.put(address, requestGeneration); + } + try { + nodeInfoClient.requestNodeInfoTiny( + new InetSocketAddress(address.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), + (info) -> SwingUtilities.invokeLater(() -> handleNodeInfo(address, requestGeneration, info))); + } catch (IOException e) { + synchronized (nodeInfoLock) { + if (nodeInfoRequestGenerations.get(address) != null + && nodeInfoRequestGenerations.get(address).longValue() == requestGeneration) { + nodeInfoRequestTimes.remove(address); + nodeInfoRequestGenerations.remove(address); + } + } + } + } + + private void handleNodeInfo(IPv6Address address, long requestGeneration, KLALBNodeInformation info) { + if (nodeInfoClosed) + return; + boolean currentRequest; + boolean nodeExists = getNodes().containsKey(address); + String deviceName = info == null ? "" : info.getDeviceName(); + if (deviceName == null || deviceName.isEmpty()) + deviceName = ""; + synchronized (nodeInfoLock) { + if (nodeInfoClosed) + return; + Long activeRequestGeneration = nodeInfoRequestGenerations.get(address); + currentRequest = activeRequestGeneration != null + && activeRequestGeneration.longValue() == requestGeneration; + if (currentRequest) { + nodeInfoRequestTimes.remove(address); + nodeInfoRequestGenerations.remove(address); + if (nodeExists) + nodeNameCache.put(address, deviceName); + else + nodeNameCache.remove(address); + if (nodeExists) + nodeNameCacheTimes.put(address, System.currentTimeMillis()); + else + nodeNameCacheTimes.remove(address); + } + } + if (currentRequest && nodeExists) { + ((InetGraphNode) getNodes().get(address)).updateLabel(); + repaint(); + } + } + + private void removeNodeInfoState(IPv6Address address) { + synchronized (nodeInfoLock) { + nodeNameCache.remove(address); + nodeNameCacheTimes.remove(address); + nodeInfoRequestTimes.remove(address); + nodeInfoRequestGenerations.remove(address); + } + } + + private void invalidateTinyNodeInfo(IPv6Address address) { + synchronized (nodeInfoLock) { + if (nodeInfoClosed) + return; + nodeNameCache.remove(address); + nodeNameCacheTimes.remove(address); + nodeInfoRequestTimes.remove(address); + nodeInfoRequestGenerations.remove(address); + } + InetGraphNode node = (InetGraphNode) getNodes().get(address); + if (node != null) { + node.updateLabel(); + } + repaint(); + requestNodeInfoTiny(address); + } + + public void close() { + synchronized (nodeInfoLock) { + if (nodeInfoClosed) + return; + nodeInfoClosed = true; + nodeNameCache.clear(); + nodeNameCacheTimes.clear(); + nodeInfoRequestTimes.clear(); + nodeInfoRequestGenerations.clear(); + } + nodeInfoRoutingProtocol.removeNodeInfoUpdateListener(nodeInfoUpdateListener); + nodeInfoClient.close(); + } + + @Override + public void removeNotify() { + super.removeNotify(); + SwingUtilities.invokeLater(() -> { + if (!isDisplayable() && getParent() == null) { + close(); + } + }); + } + private double nsPerPixel=5000L; private class InetGraphEdgeGroup extends GraphEdgeGroup{ public InetGraphEdgeGroup(GraphNode nodeA, GraphNode nodeB) { @@ -181,6 +339,8 @@ public class NetworkGraphPanel extends GraphPanel { } protected void loadNodes() { Map addr= controller.getIpv6Router().getKlalbRouteProtol().getAddresses(); + IPv6Address localAddress = controller.getIpv6Router().getLocator().getAddress(); + cacheLocalNodeName(); Set ks=addr.keySet(); for (Iterator iterator = ks.iterator(); iterator.hasNext();) { IPv6Address inet6Address = (IPv6Address) iterator.next(); @@ -188,12 +348,15 @@ public class NetworkGraphPanel extends GraphPanel { Vector2 v2pos=super.getRandomPos(); getNodes().put(inet6Address,new InetGraphNode(inet6Address,Color.BLACK,v2pos.x,v2pos.y,inet6Address.equals(controller.getIpv6Router().getLocator().getAddress()))); } + if(!inet6Address.equals(localAddress)) + requestNodeInfoTiny(inet6Address); } Set kns=getNodes().keySet(); for (Iterator iterator = kns.iterator(); iterator.hasNext();) { IPv6Address inet6Address = (IPv6Address) iterator.next(); if(!addr.containsKey(inet6Address)) { iterator.remove(); + removeNodeInfoState(inet6Address); }else { ((InetGraphNode)getNodes().get(inet6Address)).updateLabel(); } diff --git a/src/org/kne/cloud/network/klalb/ui/NodeInformationPanel.java b/src/org/kne/cloud/network/klalb/ui/NodeInformationPanel.java index 248b63e..079315d 100644 --- a/src/org/kne/cloud/network/klalb/ui/NodeInformationPanel.java +++ b/src/org/kne/cloud/network/klalb/ui/NodeInformationPanel.java @@ -15,7 +15,7 @@ import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; -import javax.swing.JTextArea; +import javax.swing.JTextArea; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; @@ -32,8 +32,9 @@ import javax.swing.JMenuItem; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.ActionEvent; +import java.awt.event.MouseListener; +import java.awt.event.ActionEvent; +import java.util.concurrent.atomic.AtomicLong; public class NodeInformationPanel extends JPanel { private IPv6Address address; @@ -41,7 +42,11 @@ public class NodeInformationPanel extends JPanel { private KLALBController controller; private Image image; - private XDefaultListModel listModel=new XDefaultListModel<>(); + private XDefaultListModel listModel=new XDefaultListModel<>(); + private XDefaultListModel extraRoutesModel=new XDefaultListModel<>(); + private volatile boolean active=true; + private final AtomicLong fullInfoGeneration=new AtomicLong(); + private final java.util.function.BiConsumer nodeInfoUpdateListener; public KLALBController getController() { return controller; } @@ -73,16 +78,20 @@ public class NodeInformationPanel extends JPanel { overviewArea.setWrapStyleWord(true); overviewArea.setFont(UIEnv.getFont().deriveFont(14.0f)); overviewArea.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); - String dname=controller.getIpv6Router().getKlalbRouteProtol().getDeviceName(address); - String ddesc=null; - if(address.equals(controller.getIpv6Router().getLocator().getAddress())&&controller.getConfigItem()!=null) { - ddesc=controller.getConfigItem().getDeviceDescription(); - if(ddesc!=null&&ddesc.isEmpty()) { - ddesc=null; - } - } - overviewArea.setText(buildOverviewText(dname, ddesc)); - panel.add(new JScrollPane(overviewArea), BorderLayout.CENTER); + overviewArea.setText(buildOverviewText(null, null)); + panel.add(new JScrollPane(overviewArea), BorderLayout.CENTER); + + JPanel extraRoutesPanel = new JPanel(new BorderLayout()); + JList extraRoutesList = new JList(extraRoutesModel); + extraRoutesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + extraRoutesList.setFont(UIEnv.getFont().deriveFont(14.0f)); + extraRoutesPanel.add(new JScrollPane(extraRoutesList), BorderLayout.CENTER); + javax.swing.JLabel extraRoutesEmptyLabel=new javax.swing.JLabel(UIEnv.getRsb().getString("noextraroutes")); + extraRoutesEmptyLabel.setText("..."); + extraRoutesEmptyLabel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + extraRoutesEmptyLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + extraRoutesPanel.add(extraRoutesEmptyLabel, BorderLayout.SOUTH); + tabbedPane.addTab(UIEnv.getRsb().getString("extraroutes"), null, extraRoutesPanel, null); JPanel panel_1 = new JPanel(); panel_1.setLayout(new BorderLayout(0, 0)); @@ -163,28 +172,59 @@ public class NodeInformationPanel extends JPanel { panel_1.add(btnNewButton, BorderLayout.SOUTH); - client=new KLALBRoutingProtocolAPIClient(controller.getIpv6Router().getKlalbRouteProtol()); - try { - client.requestNodeInfoFull(new InetSocketAddress( address.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), (info)->{ - listModel.clear(); - for (MultiProtocolSocketAddress multiProtocolSocketAddress : info.getOpenLines()) { - listModel.addElement(multiProtocolSocketAddress); - } - // 用对端返回的设备名称/描述更新概览(旧版本节点无该字段时保留原显示) - String dn=info.getDeviceName(); - if(dn==null||dn.isEmpty()) { - dn=controller.getIpv6Router().getKlalbRouteProtol().getDeviceName(address); - } - String dd=info.getDeviceDescription(); - if(dd!=null&&dd.isEmpty()) { - dd=null; - } - overviewArea.setText(buildOverviewText(dn, dd)); - }); - } catch (IOException e) { - e.printStackTrace(); - } - } + KLALBRoutingProtocol routingProtocol=controller.getIpv6Router().getKlalbRouteProtol(); + client=new KLALBRoutingProtocolAPIClient(routingProtocol); + nodeInfoUpdateListener=(updatedAddress, flags) -> { + if(active && address.equals(updatedAddress) + && (flags & org.kne.cloud.network.srv6.RouterInfoPacket.NODE_INFO_FULL_UPDATE_REQUIRED) != 0) { + requestFullInfo(overviewArea, extraRoutesEmptyLabel); + } + }; + routingProtocol.addNodeInfoUpdateListener(nodeInfoUpdateListener); + requestFullInfo(overviewArea, extraRoutesEmptyLabel); + } + + private void requestFullInfo(JTextArea overviewArea, javax.swing.JLabel extraRoutesEmptyLabel) { + final long generation=fullInfoGeneration.incrementAndGet(); + try { + client.requestNodeInfoFull(new InetSocketAddress( address.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), (info)->{ + if(!active || generation!=fullInfoGeneration.get()) return; + javax.swing.SwingUtilities.invokeLater(() -> { + if(!active || generation!=fullInfoGeneration.get()) return; + listModel.clear(); + if(info.getOpenLines()!=null) for (MultiProtocolSocketAddress item : info.getOpenLines()) listModel.addElement(item); + extraRoutesModel.clear(); + List routes=info.getExtraRoutes(); + if(routes!=null) for(String route : routes) extraRoutesModel.addElement(route); + extraRoutesEmptyLabel.setText(UIEnv.getRsb().getString("noextraroutes")); + extraRoutesEmptyLabel.setVisible(extraRoutesModel.isEmpty()); + String dd=info.getDeviceDescription(); + overviewArea.setText(buildOverviewText(info.getDeviceName(), dd==null||dd.isEmpty()?null:dd)); + }); + }); + } catch (IOException e) { + if(active) e.printStackTrace(); + } + } + + public synchronized void close() { + if (!active) + return; + active=false; + fullInfoGeneration.incrementAndGet(); + KLALBRoutingProtocol routingProtocol=controller.getIpv6Router().getKlalbRouteProtol(); + routingProtocol.removeNodeInfoUpdateListener(nodeInfoUpdateListener); + if(client!=null) { + client.close(); + client=null; + } + } + + @Override + public void removeNotify() { + close(); + super.removeNotify(); + } private String buildOverviewText(String dname,String ddesc) { StringBuilder sb=new StringBuilder(); diff --git a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java index d967389..2a5dd59 100644 --- a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java +++ b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java @@ -7,6 +7,7 @@ import java.nio.file.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import com.sun.net.httpserver.*; import com.google.gson.*; @@ -25,6 +26,7 @@ import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection; import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; import org.kne.cloud.network.srv6.NeighborInfo; import org.kne.cloud.network.srv6.RouterInfo; +import org.kne.cloud.network.srv6.RouterInfoPacket; import org.kne.cloud.network.srv6.SRv6Router; /** @@ -35,14 +37,12 @@ public class KLALBWebServer { private final KLALBProxySystem proxySystem; private final MultiProtocolSocketAddress listen; private HttpServer server; + private ExecutorService httpExecutor; 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 ScheduledExecutorService sseExecutor = createSseExecutor(); private final Set sseClients = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final AtomicBoolean running = new AtomicBoolean(false); + private final Object configUpdateLock = new Object(); public KLALBWebServer(KLALBProxySystem proxySystem, int port) { this(proxySystem, new MultiProtocolSocketAddress("http", "0.0.0.0", port)); @@ -52,15 +52,37 @@ public class KLALBWebServer { this.proxySystem = proxySystem; this.listen = listen; this.gson = proxySystem.getGson(); + this.nodeInfoUpdateListener = (address, flags) -> { + if ((flags & RouterInfoPacket.NODE_INFO_FULL_UPDATE_REQUIRED) != 0) { + nodeInfoFullRevisions.merge(address, 1L, Long::sum); + } + if ((flags & RouterInfoPacket.NODE_INFO_TINY_UPDATE_REQUIRED) != 0) { + long lifecycleGeneration; + synchronized (this) { + lifecycleGeneration = nodeInfoLifecycleGeneration; + } + invalidateTinyDeviceName(address); + KLALBController kc = this.proxySystem.getKlalbController(); + if (kc != null && kc.getIpv6Router() != null + && kc.getIpv6Router().getKlalbRouteProtol() != null) { + refreshTinyDeviceNameAsync(kc, address, lifecycleGeneration); + } + } + }; } public synchronized void start() throws IOException { if (running.get()) return; + nodeInfoFullRevisionEpoch = UUID.randomUUID().toString(); + if (sseExecutor == null || sseExecutor.isShutdown()) { + sseExecutor = createSseExecutor(); + } InetSocketAddress socketAddress = "0.0.0.0".equals(listen.getHost()) ? new InetSocketAddress(listen.getPort()) : listen.getSocketAddress(); server = HttpServer.create(socketAddress, 0); - server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + server.setExecutor(executor); // API Contexts server.createContext("/api/status", this::handleStatus); @@ -76,7 +98,13 @@ public class KLALBWebServer { // Static Files / SPA Fallback Handler server.createContext("/", this::handleStatic); - server.start(); + try { + server.start(); + httpExecutor = executor; + } catch (RuntimeException e) { + executor.shutdownNow(); + throw e; + } running.set(true); startSseBroadcaster(); @@ -84,7 +112,11 @@ public class KLALBWebServer { } public synchronized void stop() { - if (!running.get()) return; + if (!running.get()) { + closeHttpExecutor(); + closeNodeInfoClient(); + return; + } running.set(false); sseExecutor.shutdownNow(); for (HttpExchange client : sseClients) { @@ -97,9 +129,18 @@ public class KLALBWebServer { server.stop(1); server = null; } + closeHttpExecutor(); + closeNodeInfoClient(); System.out.println("KLALB Web Dashboard stopped."); } + private void closeHttpExecutor() { + if (httpExecutor != null) { + httpExecutor.shutdownNow(); + httpExecutor = null; + } + } + public boolean isRunning() { return running.get(); } @@ -456,14 +497,160 @@ public class KLALBWebServer { } private KLALBRoutingProtocolAPIClient nodeInfoClient; + private static final long TINY_NAME_REQUEST_TIMEOUT_MS = 3000L; + private final ConcurrentMap tinyDeviceNameCache = new ConcurrentHashMap<>(); + private final ConcurrentMap tinyDeviceNameCacheTimes = new ConcurrentHashMap<>(); + private final Set tinyNameRequestsInFlight = ConcurrentHashMap.newKeySet(); + private final ConcurrentMap tinyNameRequestTimes = new ConcurrentHashMap<>(); + private final ConcurrentMap tinyNameRequestGenerations = new ConcurrentHashMap<>(); + private final ConcurrentMap nodeInfoFullRevisions = new ConcurrentHashMap<>(); + private final Object tinyNameStateLock = new Object(); + private static final long TINY_NAME_CACHE_TTL_MS = 60000L; + private long nextTinyNameRequestGeneration; + private KLALBRoutingProtocol nodeInfoRoutingProtocol; + private final BiConsumer nodeInfoUpdateListener; + private long nodeInfoLifecycleGeneration; + private volatile String nodeInfoFullRevisionEpoch = UUID.randomUUID().toString(); + + private static ScheduledExecutorService createSseExecutor() { + return Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "KLALB-Web-SSE"); + t.setDaemon(true); + return t; + }); + } private synchronized KLALBRoutingProtocolAPIClient getNodeInfoClient(KLALBController kc) { + return getNodeInfoClient(kc, -1L); + } + + private synchronized KLALBRoutingProtocolAPIClient getNodeInfoClient(KLALBController kc, + long expectedLifecycleGeneration) { + if (!running.get() || (expectedLifecycleGeneration >= 0 + && expectedLifecycleGeneration != nodeInfoLifecycleGeneration) + || kc == null || kc.getIpv6Router() == null + || kc.getIpv6Router().getKlalbRouteProtol() == null) { + return null; + } if (nodeInfoClient == null) { - nodeInfoClient = new KLALBRoutingProtocolAPIClient(kc.getIpv6Router().getKlalbRouteProtol()); + nodeInfoRoutingProtocol = kc.getIpv6Router().getKlalbRouteProtol(); + nodeInfoClient = new KLALBRoutingProtocolAPIClient(nodeInfoRoutingProtocol); + nodeInfoRoutingProtocol.addNodeInfoUpdateListener(nodeInfoUpdateListener); } return nodeInfoClient; } + private synchronized void closeNodeInfoClient() { + nodeInfoLifecycleGeneration++; + if (nodeInfoRoutingProtocol != null) { + nodeInfoRoutingProtocol.removeNodeInfoUpdateListener(nodeInfoUpdateListener); + nodeInfoRoutingProtocol = null; + } + if (nodeInfoClient != null) { + nodeInfoClient.close(); + nodeInfoClient = null; + } + synchronized (tinyNameStateLock) { + tinyDeviceNameCache.clear(); + tinyDeviceNameCacheTimes.clear(); + tinyNameRequestsInFlight.clear(); + tinyNameRequestTimes.clear(); + tinyNameRequestGenerations.clear(); + } + } + + private String getTinyDeviceName(KLALBController kc, IPv6Address addr) { + return getTinyDeviceName(kc, addr, -1L); + } + + private String getTinyDeviceName(KLALBController kc, IPv6Address addr, long lifecycleGeneration) { + String cachedName; + long requestStartedAt = 0L; + long requestGeneration; + synchronized (tinyNameStateLock) { + cachedName = tinyDeviceNameCache.get(addr); + Long cachedAt = tinyDeviceNameCacheTimes.get(addr); + long now = System.currentTimeMillis(); + if (cachedName != null && cachedAt != null && now - cachedAt < TINY_NAME_CACHE_TTL_MS) return cachedName; + tinyDeviceNameCache.remove(addr); + tinyDeviceNameCacheTimes.remove(addr); + + if (tinyNameRequestsInFlight.contains(addr)) { + Long requestedAt = tinyNameRequestTimes.get(addr); + if (requestedAt != null && now - requestedAt < TINY_NAME_REQUEST_TIMEOUT_MS) { + return ""; + } + tinyNameRequestsInFlight.remove(addr); + tinyNameRequestTimes.remove(addr); + } + + if (!tinyNameRequestsInFlight.add(addr)) return ""; + requestStartedAt = now; + tinyNameRequestTimes.put(addr, requestStartedAt); + requestGeneration = ++nextTinyNameRequestGeneration; + tinyNameRequestGenerations.put(addr, requestGeneration); + } + + final long requestGenerationToken = requestGeneration; + try { + KLALBRoutingProtocolAPIClient client = lifecycleGeneration < 0 + ? getNodeInfoClient(kc) : getNodeInfoClient(kc, lifecycleGeneration); + if (client == null) { + synchronized (tinyNameStateLock) { + if (Long.valueOf(requestGenerationToken).equals(tinyNameRequestGenerations.get(addr))) { + tinyNameRequestsInFlight.remove(addr); + tinyNameRequestTimes.remove(addr); + tinyNameRequestGenerations.remove(addr); + } + } + return ""; + } + client.requestNodeInfoTiny( + new InetSocketAddress(addr.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), + info -> { + synchronized (tinyNameStateLock) { + Long activeRequestGeneration = tinyNameRequestGenerations.get(addr); + if (!Long.valueOf(requestGenerationToken).equals(activeRequestGeneration)) return; + tinyDeviceNameCache.put(addr, + info != null && info.getDeviceName() != null ? info.getDeviceName() : ""); + tinyDeviceNameCacheTimes.put(addr, System.currentTimeMillis()); + tinyNameRequestsInFlight.remove(addr); + tinyNameRequestTimes.remove(addr); + tinyNameRequestGenerations.remove(addr); + } + }); + } catch (IOException e) { + synchronized (tinyNameStateLock) { + Long activeRequestGeneration = tinyNameRequestGenerations.get(addr); + if (Long.valueOf(requestGenerationToken).equals(activeRequestGeneration)) { + tinyNameRequestsInFlight.remove(addr); + tinyNameRequestTimes.remove(addr); + tinyNameRequestGenerations.remove(addr); + } + } + } + return ""; + } + + private void refreshTinyDeviceNameAsync(KLALBController kc, IPv6Address address, long lifecycleGeneration) { + CompletableFuture.runAsync(() -> { + synchronized (this) { + if (!running.get() || nodeInfoLifecycleGeneration != lifecycleGeneration) return; + getTinyDeviceName(kc, address, lifecycleGeneration); + } + }); + } + + private void invalidateTinyDeviceName(IPv6Address address) { + synchronized (tinyNameStateLock) { + tinyDeviceNameCache.remove(address); + tinyDeviceNameCacheTimes.remove(address); + tinyNameRequestsInFlight.remove(address); + tinyNameRequestTimes.remove(address); + tinyNameRequestGenerations.remove(address); + } + } + private void handleNodeInfo(HttpExchange exchange) throws IOException { if (handleCorsPreflight(exchange)) return; if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { @@ -500,18 +687,23 @@ public class KLALBWebServer { if (isSelf) { // 本机:描述直接取本地控制器配置 - KLALBControllerConfigItem cfg = proxySystem.getControllerConfig(); + KLALBController.PublishedNodeInfo published = kc.getPublishedNodeInfo(); resp.addProperty("isSelf", true); - String dname = router.getDeviceName() != null ? router.getDeviceName() - : (cfg != null && cfg.getDeviceName() != null ? cfg.getDeviceName() : ""); - resp.addProperty("deviceName", dname); + resp.addProperty("deviceName", published.getDeviceName() != null ? published.getDeviceName() : ""); resp.addProperty("deviceDescription", - cfg != null && cfg.getDeviceDescription() != null ? cfg.getDeviceDescription() : ""); + published.getDeviceDescription() != null ? published.getDeviceDescription() : ""); JsonArray lines = new JsonArray(); - for (MultiProtocolSocketAddress mpsa : kc.getExternalEndpoints()) { + for (MultiProtocolSocketAddress mpsa : published.getExternalEndpoints()) { lines.add(new JsonPrimitive(mpsa.toString())); } resp.add("openLines", lines); + JsonArray extraRoutes = new JsonArray(); + if (published.getExtraRoutes() != null) { + for (String route : published.getExtraRoutes()) { + extraRoutes.add(new JsonPrimitive(route != null ? route : "")); + } + } + resp.add("extraRoutes", extraRoutes); sendJsonResponse(exchange, 200, resp); return; } @@ -520,7 +712,12 @@ public class KLALBWebServer { resp.addProperty("isSelf", false); CompletableFuture future = new CompletableFuture<>(); try { - getNodeInfoClient(kc).requestNodeInfoFull( + KLALBRoutingProtocolAPIClient client = getNodeInfoClient(kc); + if (client == null) { + sendError(exchange, 503, "Node info service stopped"); + return; + } + client.requestNodeInfoFull( new InetSocketAddress(target.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), info -> future.complete(info)); @@ -533,8 +730,8 @@ public class KLALBWebServer { String dname = info != null && info.getDeviceName() != null && !info.getDeviceName().isEmpty() ? info.getDeviceName() - : router.getKlalbRouteProtol().getDeviceName(target); - resp.addProperty("deviceName", dname != null ? dname : ""); + : ""; + resp.addProperty("deviceName", dname); resp.addProperty("deviceDescription", info != null && info.getDeviceDescription() != null ? info.getDeviceDescription() : ""); resp.addProperty("reachable", info != null); @@ -545,11 +742,19 @@ public class KLALBWebServer { } } resp.add("openLines", lines); + JsonArray extraRoutes = new JsonArray(); + if (info != null && info.getExtraRoutes() != null) { + for (String route : info.getExtraRoutes()) { + extraRoutes.add(new JsonPrimitive(route != null ? route : "")); + } + } + resp.add("extraRoutes", extraRoutes); } catch (Exception e) { resp.addProperty("deviceName", ""); resp.addProperty("deviceDescription", ""); resp.addProperty("reachable", false); resp.add("openLines", new JsonArray()); + resp.add("extraRoutes", new JsonArray()); } sendJsonResponse(exchange, 200, resp); } @@ -584,6 +789,17 @@ public class KLALBWebServer { KLALBRoutingProtocol rproto = kc.getIpv6Router().getKlalbRouteProtol(); Map addrs = rproto.getAddresses(); IPv6Address selfAddr = kc.getIpv6Router().getLocator().getAddress(); + KLALBController.PublishedNodeInfo published = kc.getPublishedNodeInfo(); + Set activeAddresses = addrs == null + ? Collections.emptySet() : new HashSet(addrs.keySet()); + synchronized (tinyNameStateLock) { + tinyDeviceNameCache.keySet().removeIf(address -> !activeAddresses.contains(address)); + tinyDeviceNameCacheTimes.keySet().removeIf(address -> !activeAddresses.contains(address)); + tinyNameRequestsInFlight.removeIf(address -> !activeAddresses.contains(address)); + tinyNameRequestTimes.keySet().removeIf(address -> !activeAddresses.contains(address)); + tinyNameRequestGenerations.keySet().removeIf(address -> !activeAddresses.contains(address)); + } + nodeInfoFullRevisions.keySet().removeIf(address -> !activeAddresses.contains(address)); if (addrs != null) { for (IPv6Address addr : addrs.keySet()) { @@ -592,8 +808,13 @@ public class KLALBWebServer { nodeObj.addProperty("address", addr.toString()); nodeObj.addProperty("compressedAddress", addr.toCompressedString()); nodeObj.addProperty("isSelf", addr.equals(selfAddr)); - String dname = rproto.getDeviceName(addr); + String dname = addr.equals(selfAddr) ? kc.getIpv6Router().getDeviceName() + : getTinyDeviceName(kc, addr); nodeObj.addProperty("deviceName", dname != null ? dname : ""); + long fullRevision = addr.equals(selfAddr) ? published.getFullRevision() + : nodeInfoFullRevisions.getOrDefault(addr, 0L); + nodeObj.addProperty("nodeInfoFullRevision", fullRevision); + nodeObj.addProperty("nodeInfoFullRevisionEpoch", nodeInfoFullRevisionEpoch); nodesArray.add(nodeObj); } } @@ -644,21 +865,33 @@ public class KLALBWebServer { sendError(exchange, 404, "Configuration not found"); } } else if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) { + synchronized (configUpdateLock) { String body = readRequestBody(exchange); try { JsonObject json = new JsonParser().parse(body).getAsJsonObject(); KLALBControllerConfigItem current = proxySystem.getControllerConfig(); if (current != null) { + String oldDeviceName = current.getDeviceName(); + String oldDeviceDescription = current.getDeviceDescription(); + List oldExternalEndpoints = current.getExternalEndpoints() == null + ? null + : new ArrayList(current.getExternalEndpoints()); + List oldExtraRoutes = current.getExtraRoutes() == null + ? null : new ArrayList(current.getExtraRoutes()); + String newDeviceName = oldDeviceName; + String newDeviceDescription = oldDeviceDescription; + List newExternalEndpoints = current.getExternalEndpoints(); + List newExtraRoutes = current.getExtraRoutes(); if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) { - current.setDeviceName(json.get("deviceName").getAsString()); + newDeviceName = json.get("deviceName").getAsString(); } else if (json.has("DeviceName") && !json.get("DeviceName").isJsonNull()) { - current.setDeviceName(json.get("DeviceName").getAsString()); + newDeviceName = json.get("DeviceName").getAsString(); } if (json.has("deviceDescription") && !json.get("deviceDescription").isJsonNull()) { - current.setDeviceDescription(json.get("deviceDescription").getAsString()); + newDeviceDescription = json.get("deviceDescription").getAsString(); } else if (json.has("DeviceDescription") && !json.get("DeviceDescription").isJsonNull()) { - current.setDeviceDescription(json.get("DeviceDescription").getAsString()); + newDeviceDescription = json.get("DeviceDescription").getAsString(); } if (json.has("language") && !json.get("language").isJsonNull()) { @@ -733,7 +966,7 @@ public class KLALBWebServer { list.add(new MultiProtocolSocketAddress(el.getAsString())); } } - current.setExternalEndpoints(list); + newExternalEndpoints = list; } if (json.has("autoConnections") || json.has("AutoConnections") || json.has("connectLineTable") || json.has("ConnectLineTable")) { @@ -783,7 +1016,7 @@ public class KLALBWebServer { list.add(el.getAsString()); } } - current.setExtraRoutes(list); + newExtraRoutes = list; } if (json.has("networkInterfaceExcepts") || json.has("NetworkInterfaceExcepts")) { @@ -845,6 +1078,23 @@ public class KLALBWebServer { current.setDenyExternalEndpointBroadcast(json.get("denyLineTableBroadcast").getAsBoolean()); } + boolean deviceNameChanged = !Objects.equals(oldDeviceName, newDeviceName); + boolean deviceDescriptionChanged = !Objects.equals(oldDeviceDescription, newDeviceDescription); + boolean externalEndpointsChanged = !Objects.equals(oldExternalEndpoints, newExternalEndpoints); + boolean extraRoutesChanged = !Objects.equals(oldExtraRoutes, newExtraRoutes); + if (deviceNameChanged || deviceDescriptionChanged || externalEndpointsChanged || extraRoutesChanged) { + KLALBController kc = proxySystem.getKlalbController(); + if (kc != null) { + kc.publishNodeInfo(newDeviceName, newDeviceDescription, newExternalEndpoints, + newExtraRoutes); + } else { + current.setDeviceName(newDeviceName); + current.setDeviceDescription(newDeviceDescription); + current.setExternalEndpoints(newExternalEndpoints); + current.setExtraRoutes(newExtraRoutes); + } + } + // Trigger GUI save consumer or save directly if (proxySystem.getKLALBGUI() != null && proxySystem.getKLALBGUI().getSaveComsumer() != null) { proxySystem.getKLALBGUI().getSaveComsumer().accept(proxySystem.getConfig()); @@ -862,6 +1112,7 @@ public class KLALBWebServer { } catch (Exception e) { sendError(exchange, 400, "Failed to update configuration: " + e.getMessage()); } + } } else { sendError(exchange, 405, "Method not allowed"); } diff --git a/src/org/kne/cloud/network/srv6/JsonDataPacket.java b/src/org/kne/cloud/network/srv6/JsonDataPacket.java index fef11b4..d32bfb5 100644 --- a/src/org/kne/cloud/network/srv6/JsonDataPacket.java +++ b/src/org/kne/cloud/network/srv6/JsonDataPacket.java @@ -33,7 +33,8 @@ public class JsonDataPacket extends KLALBRoutingProtocolPacket implements Serial private String data; - private static final int HEADER_LENGTH=3; + private static final int HEADER_LENGTH=3; + private static final int MAX_JSON_PAYLOAD_LENGTH=65535-HEADER_LENGTH; private static Gson gson; static{ @@ -73,9 +74,12 @@ public class JsonDataPacket extends KLALBRoutingProtocolPacket implements Serial @Override - public void writeToChannel(WritableByteChannel dto) throws IOException { - byte[]bta=data.getBytes(Charset.forName("UTF-8")); - getHeader().putChar(1,(char) bta.length); + public void writeToChannel(WritableByteChannel dto) throws IOException { + byte[]bta=data.getBytes(Charset.forName("UTF-8")); + if(bta.length>MAX_JSON_PAYLOAD_LENGTH) { + throw new IOException("JSON payload exceeds " + MAX_JSON_PAYLOAD_LENGTH + " bytes"); + } + getHeader().putChar(1,(char) bta.length); super.writeToChannel(dto); dto.write(ByteBuffer.wrap(bta)); } diff --git a/src/org/kne/cloud/network/srv6/KLALBNodeInformation.java b/src/org/kne/cloud/network/srv6/KLALBNodeInformation.java index d38f5d6..6a94ba6 100644 --- a/src/org/kne/cloud/network/srv6/KLALBNodeInformation.java +++ b/src/org/kne/cloud/network/srv6/KLALBNodeInformation.java @@ -1,6 +1,7 @@ package org.kne.cloud.network.srv6; import java.util.List; +import java.util.ArrayList; import org.kne.cloud.network.MultiProtocolSocketAddress; @@ -11,13 +12,20 @@ public class KLALBNodeInformation { private List openLines; private String deviceName; private String deviceDescription; + private List extraRoutes; public KLALBNodeInformation(List openLines, String deviceName, String deviceDescription) { + this(openLines, deviceName, deviceDescription, new ArrayList()); + } + + public KLALBNodeInformation(List openLines, String deviceName, + String deviceDescription, List extraRoutes) { super(); this.openLines = openLines; this.deviceName = deviceName; this.deviceDescription = deviceDescription; + this.extraRoutes = extraRoutes == null ? new ArrayList() : extraRoutes; } public List getOpenLines() { @@ -44,9 +52,17 @@ public class KLALBNodeInformation { this.deviceDescription = deviceDescription; } + public List getExtraRoutes() { + return extraRoutes; + } + + public void setExtraRoutes(List extraRoutes) { + this.extraRoutes = extraRoutes == null ? new ArrayList() : extraRoutes; + } + @Override public String toString() { return "KLALBNodeInformation [openLines=" + openLines + ", deviceName=" + deviceName - + ", deviceDescription=" + deviceDescription + "]"; + + ", deviceDescription=" + deviceDescription + ", extraRoutes=" + extraRoutes + "]"; } } diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocol.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocol.java index 1a9980f..d3c2e52 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocol.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocol.java @@ -13,15 +13,18 @@ import java.nio.channels.Channels; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; -import java.util.List; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; import org.kne.cloud.network.ipv6.IPv6Address; @@ -41,8 +44,12 @@ public class KLALBRoutingProtocol extends Thread{ private static final boolean debug = false; - private RouterInfo selfRouterInfo; - private Map netmap=new ConcurrentHashMap<>(); + private RouterInfo selfRouterInfo; + private Map netmap=new ConcurrentHashMap<>(); + private final AtomicLong selfCreateTime = new AtomicLong(); + private final AtomicInteger pendingNodeInfoUpdateFlags = new AtomicInteger(); + private static final int NODE_INFO_UPDATE_REPLAY_WINDOW_SIZE = 64; + private final Map> seenNodeInfoUpdates = new HashMap<>(); public Map getNetmap() { return netmap; @@ -84,15 +91,67 @@ public class KLALBRoutingProtocol extends Thread{ private KLALBVirtualRawSocket ds = null; private ReentrantLock sendLock=new ReentrantLock(); - private Set>receivers=new CopyOnWriteArraySet>(); + private Set>receivers=new CopyOnWriteArraySet>(); + private Set> nodeInfoUpdateListeners = new CopyOnWriteArraySet>(); public void addReceiver(BiConsumer rec) { receivers.add(rec); } - public void removeReceiver(BiConsumer rec) { - receivers.remove(rec); - } + public void removeReceiver(BiConsumer rec) { + receivers.remove(rec); + } + + public void addNodeInfoUpdateListener(BiConsumer listener) { + nodeInfoUpdateListeners.add(listener); + } + + public void removeNodeInfoUpdateListener(BiConsumer listener) { + nodeInfoUpdateListeners.remove(listener); + } + + public void announceNodeInfoUpdate(int flags) { + int validFlags = flags & (RouterInfoPacket.NODE_INFO_TINY_UPDATE_REQUIRED + | RouterInfoPacket.NODE_INFO_FULL_UPDATE_REQUIRED); + if(validFlags != 0) { + pendingNodeInfoUpdateFlags.getAndUpdate(old -> old | validFlags); + } + } + + private boolean markNodeInfoUpdateSeen(IPv6Address origin, long createTime) { + synchronized(seenNodeInfoUpdates) { + LinkedHashSet createTimes = seenNodeInfoUpdates.get(origin); + if(createTimes == null) { + createTimes = new LinkedHashSet<>(); + seenNodeInfoUpdates.put(origin, createTimes); + } + if(!createTimes.add(createTime)) { + return false; + } + while(createTimes.size() > NODE_INFO_UPDATE_REPLAY_WINDOW_SIZE) { + createTimes.remove(createTimes.iterator().next()); + } + return true; + } + } + + private void removeNodeInfoUpdateWindow(IPv6Address origin) { + synchronized(seenNodeInfoUpdates) { + seenNodeInfoUpdates.remove(origin); + } + } + + private void notifyNodeInfoUpdate(IPv6Address origin, int flags) { + for(BiConsumer listener : nodeInfoUpdateListeners) { + try { + listener.accept(origin, flags); + } catch(Throwable e) { + if(debug) { + e.printStackTrace(); + } + } + } + } @Override public void run() { @@ -103,11 +162,22 @@ public class KLALBRoutingProtocol extends Thread{ new Thread(()->{ Thread.currentThread().setName("KLALB路由协议发送线程"); - while(true) { - try { - selfRouterInfo = getSelfRouterInfo(); - RouterInfo oslf=netmap.put(selfRouterInfo.getLocator().getAddress(), selfRouterInfo); - noticeUpdate(); + while(true) { + try { + selfRouterInfo = getSelfRouterInfo(); + RouterInfo oslf=netmap.put(selfRouterInfo.getLocator().getAddress(), selfRouterInfo); + noticeUpdate(); + int nodeInfoUpdateFlags = pendingNodeInfoUpdateFlags.getAndSet(0); + if(nodeInfoUpdateFlags != 0) { + RouterInfoPacket updatePacket = new RouterInfoPacket(getSelfRouterInfo(), true, -1); + updatePacket.setNodeInfoUpdateFlags(nodeInfoUpdateFlags); + try { + floodPacket(null, updatePacket); + } catch(IOException e) { + pendingNodeInfoUpdateFlags.getAndUpdate(old -> old | nodeInfoUpdateFlags); + e.printStackTrace(); + } + } /*if(oslf==null||(!selfRouterInfo.equals(oslf))) { long cur=System.nanoTime(); if(cur-floodTimer2>5000000000L) { @@ -147,10 +217,12 @@ public class KLALBRoutingProtocol extends Thread{ if(val.getAsn()==router.getASN()) { timeout=2000000000L; } - if(val.checkTimeOut(timeout)) { - iterator.remove(); - noticeUpdate(); - } + if(val.checkTimeOut(timeout)) { + if(netmap.remove(type.getKey(), val)) { + removeNodeInfoUpdateWindow(type.getKey()); + noticeUpdate(); + } + } /*List ads=val.getNeighborAddresses(); for (Iterator iterator2 = ads.iterator(); iterator2.hasNext();) { Inet6Address address=iterator2.next().getLocator().getAddress(); @@ -216,7 +288,7 @@ public class KLALBRoutingProtocol extends Thread{ ds.receive(dgp); //System.out.println(Arrays.toString( Arrays.copyOf( dgp.getData(),dgp.getLength()))); ByteArrayInputStream bi=new ByteArrayInputStream(dgp.getData(),0,dgp.getLength()); - KLALBRoutingProtocolPacket kp=KLALBRoutingProtocolPacket.readKLALBPacketFromChannel(Channels.newChannel(bi)); + KLALBRoutingProtocolPacket kp=KLALBRoutingProtocolPacket.readKLALBPacketFromChannel(Channels.newChannel(bi), dgp.getLength()); switch(kp.getType()) { case KLALBRoutingProtocolPacket.RINFO_REQ: @@ -234,28 +306,34 @@ public class KLALBRoutingProtocol extends Thread{ break; case KLALBRoutingProtocolPacket.RINFO: - RouterInfoPacket rifp=(RouterInfoPacket) kp; - RouterInfo rif=rifp.getRinfo(); - RouterInfo oldrif=netmap.get(rif.getLocator().getAddress()); + RouterInfoPacket rifp=(RouterInfoPacket) kp; + RouterInfo rif=rifp.getRinfo(); + RouterInfo oldrif=netmap.get(rif.getLocator().getAddress()); + IPv6Address origin = rif.getLocator().getAddress(); + int nodeInfoUpdateFlags = rifp.getNodeInfoUpdateFlags(); + boolean newNodeInfoUpdate = nodeInfoUpdateFlags != 0 + && markNodeInfoUpdateSeen(origin, rif.getCreateTime()); if(debug) System.out.println(rif); - if(oldrif==null||oldrif.getCreateTime()links=router.getLinkTabel(); Object[] nls=links.toArray(); - RouterInfo ri=new RouterInfo(System.currentTimeMillis(),router.getLocator(),router.getASN()); - ri.setDeviceName(router.getDeviceName()); + long currentTime = System.currentTimeMillis(); + long createTime = selfCreateTime.updateAndGet(previous -> Math.max(currentTime, previous + 1)); + RouterInfo ri=new RouterInfo(createTime,router.getLocator(),router.getASN()); for(int i=0;i> getPaths() { return paths; diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java index 1a201e7..f0915da 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java @@ -36,7 +36,7 @@ public class KLALBRoutingProtocolAPIClient { List connects = (List) dataobj.getData(); List connectsm = new ArrayList( connects == null ? 0 : connects.size()); - if (connects != null) { + if (connects != null) { for (Object open : connects) { if (open instanceof MultiProtocolSocketAddress) { connectsm.add((MultiProtocolSocketAddress) open); @@ -45,10 +45,14 @@ public class KLALBRoutingProtocolAPIClient { } } - } - // 组装节点信息(线路 + 设备名称 + 设备描述,精简模式下线路与描述为 null) - KLALBNodeInformation info = new KLALBNodeInformation(connectsm, dataobj.getDeviceName(), - dataobj.getDeviceDescription()); + } + List extraRoutes = dataobj.getExtraRoutes(); + if(extraRoutes == null) { + extraRoutes = new ArrayList(); + } + // 组装节点信息(线路 + 设备名称 + 设备描述,精简模式下线路与描述为 null) + KLALBNodeInformation info = new KLALBNodeInformation(connectsm, dataobj.getDeviceName(), + dataobj.getDeviceDescription(), extraRoutes); ((Consumer) relate.getUserCallback()).accept(info); } break; @@ -122,4 +126,4 @@ class KLALBRoutingProtocolAPIClientReleaser extends Releaser(fullPublished.getExternalEndpoints())); + json.setDeviceName(fullPublished.getDeviceName());// 附带本机设备名称 + json.setDeviceDescription(fullPublished.getDeviceDescription());// 附带本机设备描述 + json.setExtraRoutes(new ArrayList(fullPublished.getExtraRoutes())); + routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(json),addr); break; } } catch (IOException e) { @@ -85,4 +88,4 @@ class KLALBRoutingProtocolAPIServerReleaser extends Releaser resource) { routingProtocol.removeReceiver(resource); } -} \ No newline at end of file +} diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java index ca8f1b5..0e8e80c 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java @@ -1,6 +1,7 @@ package org.kne.cloud.network.srv6; -import java.util.UUID; +import java.util.UUID; +import java.util.List; public class KLALBRoutingProtocolJsonData { public static final String NODE_INFO_TINY_REQ="nodeinfotinyreq";// 精简查询:仅设备名称 @@ -10,8 +11,9 @@ public class KLALBRoutingProtocolJsonData { private String type; private UUID uuid; private Object data; - private String deviceName;// 对端设备名称 - private String deviceDescription;// 对端设备描述 + private String deviceName;// 对端设备名称 + private String deviceDescription;// 对端设备描述 + private List extraRoutes; public String getDeviceName() { return deviceName; } @@ -21,9 +23,15 @@ public class KLALBRoutingProtocolJsonData { public String getDeviceDescription() { return deviceDescription; } - public void setDeviceDescription(String deviceDescription) { - this.deviceDescription = deviceDescription; - } + public void setDeviceDescription(String deviceDescription) { + this.deviceDescription = deviceDescription; + } + public List getExtraRoutes() { + return extraRoutes; + } + public void setExtraRoutes(List extraRoutes) { + this.extraRoutes = extraRoutes; + } public String getType() { return type; } @@ -41,7 +49,8 @@ public class KLALBRoutingProtocolJsonData { result = prime * result + ((type == null) ? 0 : type.hashCode()); result = prime * result + ((uuid == null) ? 0 : uuid.hashCode()); result = prime * result + ((deviceName == null) ? 0 : deviceName.hashCode()); - result = prime * result + ((deviceDescription == null) ? 0 : deviceDescription.hashCode()); + result = prime * result + ((deviceDescription == null) ? 0 : deviceDescription.hashCode()); + result = prime * result + ((extraRoutes == null) ? 0 : extraRoutes.hashCode()); return result; } @Override @@ -76,9 +85,14 @@ public class KLALBRoutingProtocolJsonData { if (deviceDescription == null) { if (other.deviceDescription != null) return false; - } else if (!deviceDescription.equals(other.deviceDescription)) - return false; - return true; + } else if (!deviceDescription.equals(other.deviceDescription)) + return false; + if (extraRoutes == null) { + if (other.extraRoutes != null) + return false; + } else if (!extraRoutes.equals(other.extraRoutes)) + return false; + return true; } public KLALBRoutingProtocolJsonData(String type, UUID uuid, Object data) { super(); @@ -88,8 +102,8 @@ public class KLALBRoutingProtocolJsonData { } @Override public String toString() { - return "KLALBRoutingProtocolJsonData [type=" + type + ", uuid=" + uuid + ", data=" + data + ", deviceName=" - + deviceName + ", deviceDescription=" + deviceDescription + "]"; + return "KLALBRoutingProtocolJsonData [type=" + type + ", uuid=" + uuid + ", data=" + data + ", deviceName=" + + deviceName + ", deviceDescription=" + deviceDescription + ", extraRoutes=" + extraRoutes + "]"; } } diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolPacket.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolPacket.java index 6986878..459ed7f 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolPacket.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolPacket.java @@ -95,8 +95,12 @@ public abstract class KLALBRoutingProtocolPacket extends NetworkPacket { return readKLALBPacketFromChannel(Channels.newChannel(in)); } - public static KLALBRoutingProtocolPacket readKLALBPacketFromChannel(ReadableByteChannel in) throws IOException { - while(true) { + public static KLALBRoutingProtocolPacket readKLALBPacketFromChannel(ReadableByteChannel in) throws IOException { + return readKLALBPacketFromChannel(in, -1); + } + + public static KLALBRoutingProtocolPacket readKLALBPacketFromChannel(ReadableByteChannel in, long packetLength) throws IOException { + while(true) { ByteBuffer bb=NetworkPacket.bufferAllocator.allocate(40); bb.limit(1); while(bb.hasRemaining()){ @@ -109,18 +113,20 @@ public abstract class KLALBRoutingProtocolPacket extends NetworkPacket { KLALBRoutingProtocolPacket klp; switch(type) { - case RINFO_REQ: - klp=new RouterInfoRequestPacket(bb); - klp.readFromChannel(in); - return klp; - case RINFO: - klp=new RouterInfoPacket(bb); - klp.readFromChannel(in); - return klp; - case JSON_DATA: - klp=new JsonDataPacket(bb); - klp.readFromChannel(in); - return klp; + case RINFO_REQ: + klp=new RouterInfoRequestPacket(bb); + if(packetLength < 0) klp.readFromChannel(in); + else klp.readFromChannel(in, packetLength); + return klp; + case RINFO: + klp=new RouterInfoPacket(bb); + if(packetLength < 0) klp.readFromChannel(in); + else klp.readFromChannel(in, packetLength); + return klp; + case JSON_DATA: + klp=new JsonDataPacket(bb); + klp.readFromChannel(in); + return klp; } //throw new StreamCorruptedException("unknown package type:"+type); System.err.println("ignore unknown KLALBRoutingProtocolPacket type:"+type); diff --git a/src/org/kne/cloud/network/srv6/RouterInfo.java b/src/org/kne/cloud/network/srv6/RouterInfo.java index ae46e1f..6e2a149 100644 --- a/src/org/kne/cloud/network/srv6/RouterInfo.java +++ b/src/org/kne/cloud/network/srv6/RouterInfo.java @@ -60,15 +60,15 @@ public class RouterInfo implements Serializable{ return Objects.equals(locator, other.locator) && Objects.equals(neighborAddresses, other.neighborAddresses); } private List neighborAddresses=new ArrayList<>(); - private String deviceName=""; // 设备名称(广播时携带,描述不广播) - private long createTime; + private long createTime; public long getCreateTime() { return createTime; } - private static final long INFO_UPDATETIME=10000000000L; - private static final long INFO_TIMEOUT=60000000000L; + private static final long INFO_UPDATETIME=10000000000L; + private static final long INFO_TIMEOUT=60000000000L; + private static final int MAX_NEIGHBOR_COUNT = 512; private long putTime=System.nanoTime(); public boolean checkUpdateTime() { return System.nanoTime()-putTime>INFO_UPDATETIME; @@ -91,31 +91,28 @@ public class RouterInfo implements Serializable{ return neighborAddresses; } - public String getDeviceName() { - return deviceName; - } - - public void setDeviceName(String deviceName) { - this.deviceName = deviceName; - } - - public RouterInfo( long createTime,IPv6AddressGroup locator,long asn) { + public RouterInfo( long createTime,IPv6AddressGroup locator,long asn) { this.createTime = createTime; this.locator=locator; this.asn=asn; } - public RouterInfo() { - } - public void writeToStream(DataOutputStream out) throws IOException { - locator.writeToStream(out); - out.writeLong(asn); - out.writeLong(createTime); - out.writeInt(neighborAddresses.size()); + public RouterInfo() { + } + public void writeToStream(DataOutputStream out) throws IOException { + int neighborCount = neighborAddresses == null ? -1 : neighborAddresses.size(); + if(neighborCount<0 || neighborCount>MAX_NEIGHBOR_COUNT) { + throw new IOException("Invalid neighbor count: " + neighborCount); + } + locator.writeToStream(out); + out.writeLong(asn); + out.writeLong(createTime); + out.writeInt(neighborCount); for( NeighborInfo neighborInfo :neighborAddresses) { neighborInfo.writeToStream(out); } - out.writeUTF(deviceName==null?"":deviceName); + // Keep the legacy UTF slot so older RINFO decoders stay aligned. + out.writeUTF(""); } protected void writeToChannel(WritableByteChannel dto) throws IOException { @@ -127,16 +124,20 @@ public class RouterInfo implements Serializable{ public void readFromStream(DataInputStream in) throws IOException { locator=new IPv6AddressGroup(in); asn=in.readLong(); - createTime=in.readLong(); - int size=in.readInt(); - neighborAddresses=new ArrayList<>(size); + createTime=in.readLong(); + int size=in.readInt(); + if(size<0 || size>MAX_NEIGHBOR_COUNT) { + throw new IOException("Invalid neighbor count: " + size); + } + neighborAddresses=new ArrayList<>(size); for (int i = 0; i < size; i++) { NeighborInfo nif=new NeighborInfo(); nif.readFromStream(in); neighborAddresses.add(nif); } - deviceName=in.readUTF(); + // Consume the legacy device-name slot; names are no longer broadcast here. + in.readUTF(); } } diff --git a/src/org/kne/cloud/network/srv6/RouterInfoPacket.java b/src/org/kne/cloud/network/srv6/RouterInfoPacket.java index 31ef280..04a2c60 100644 --- a/src/org/kne/cloud/network/srv6/RouterInfoPacket.java +++ b/src/org/kne/cloud/network/srv6/RouterInfoPacket.java @@ -1,7 +1,8 @@ package org.kne.cloud.network.srv6; import java.io.DataInputStream; -import java.io.DataOutputStream; +import java.io.DataOutputStream; +import java.io.ByteArrayInputStream; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; @@ -22,7 +23,12 @@ import java.util.Set; import org.kne.cloud.network.NetworkPacket; import org.kne.cloud.network.ipv6.IPv6AddressGroup; -public class RouterInfoPacket extends KLALBRoutingProtocolPacket implements Serializable{ +public class RouterInfoPacket extends KLALBRoutingProtocolPacket implements Serializable{ + public static final int NODE_INFO_TINY_UPDATE_REQUIRED = 1; + public static final int NODE_INFO_FULL_UPDATE_REQUIRED = 2; + private static final int NODE_INFO_UPDATE_FLAGS_MASK = NODE_INFO_TINY_UPDATE_REQUIRED + | NODE_INFO_FULL_UPDATE_REQUIRED; + private static final int NODE_INFO_UPDATE_MAGIC = 0xA5; public RouterInfoPacket(RouterInfo routerInfo, boolean flood,long asn) { super(RINFO); @@ -34,7 +40,8 @@ public class RouterInfoPacket extends KLALBRoutingProtocolPacket implements Seri super(bb); } - private RouterInfo rinfo; + private RouterInfo rinfo; + private transient int nodeInfoUpdateFlags; public boolean isFlood() { return (getHeader().get(1)&1)==1; @@ -44,9 +51,17 @@ public class RouterInfoPacket extends KLALBRoutingProtocolPacket implements Seri return getHeader().getLong(2); } - public RouterInfo getRinfo() { - return rinfo; - } + public RouterInfo getRinfo() { + return rinfo; + } + + public int getNodeInfoUpdateFlags() { + return nodeInfoUpdateFlags; + } + + public void setNodeInfoUpdateFlags(int flags) { + nodeInfoUpdateFlags = flags & NODE_INFO_UPDATE_FLAGS_MASK; + } @Override protected long getHeaderSize() { return super.getHeaderSize()+9; @@ -65,15 +80,46 @@ public class RouterInfoPacket extends KLALBRoutingProtocolPacket implements Seri @Override - public void writeToChannel(WritableByteChannel dto) throws IOException { - super.writeToChannel(dto); - rinfo.writeToChannel(dto); - } + public void writeToChannel(WritableByteChannel dto) throws IOException { + super.writeToChannel(dto); + rinfo.writeToChannel(dto); + int flags = nodeInfoUpdateFlags & NODE_INFO_UPDATE_FLAGS_MASK; + if(flags != 0) { + ByteBuffer extension = ByteBuffer.wrap(new byte[] {(byte) NODE_INFO_UPDATE_MAGIC, (byte) flags}); + while(extension.hasRemaining()) { + dto.write(extension); + } + } + } @Override public void readFromChannel(ReadableByteChannel din, long length) throws IOException { - super.readFromChannel(din, length); - rinfo=new RouterInfo(); - rinfo.readFromChannel(din, length); + super.readFromChannel(din, length); + rinfo=new RouterInfo(); + if(length < 0) { + rinfo.readFromChannel(din, length); + nodeInfoUpdateFlags = 0; + return; + } + long payloadLength = length - getHeaderSize(); + if(payloadLength < 0 || payloadLength > Integer.MAX_VALUE) { + throw new IOException("Invalid RINFO packet length: " + length); + } + byte[] payload = new byte[(int) payloadLength]; + ByteBuffer payloadBuffer = ByteBuffer.wrap(payload); + while(payloadBuffer.hasRemaining()) { + if(din.read(payloadBuffer) == -1) { + throw new java.io.EOFException(); + } + } + ByteArrayInputStream payloadInput = new ByteArrayInputStream(payload); + rinfo.readFromStream(new DataInputStream(payloadInput)); + nodeInfoUpdateFlags = 0; + int marker = payloadInput.read(); + int flags = payloadInput.read(); + int trailing = payloadInput.read(); + if(marker == NODE_INFO_UPDATE_MAGIC && flags >= 0 && trailing == -1) { + nodeInfoUpdateFlags = flags & NODE_INFO_UPDATE_FLAGS_MASK; + } } } -- 2.39.5 From b6acc6d50999480ad78e07beaac5dc33871c2ec3 Mon Sep 17 00:00:00 2001 From: SerinaNya <34389622+SerinaNya@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:24:20 +0800 Subject: [PATCH 2/4] fix(config): sync Swing settings after Web updates Add revisioned configuration events and detached commits so Web changes refresh clean Swing forms without overwriting unsaved local edits. Preserve configuration formatting and document the synchronized update workflow. --- AGENTS.md | 3 +- src/klalb_en_US.properties | 8 +- src/klalb_zh_CN.properties | 8 +- .../cloud/network/klalb/KLALBController.java | 58 ++- .../cloud/network/klalb/KLALBProxySystem.java | 331 +++++++++++++++--- .../network/klalb/ui/KLALBStateGUI3.java | 246 ++++++++++--- .../network/klalb/web/KLALBWebServer.java | 53 ++- 7 files changed, 577 insertions(+), 130 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7d962ff..c323de6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,8 +46,9 @@ pnpm dev - `KLALBConfigItem` is a polymorphic JSON array keyed by `Type`. Adding a type requires a subclass and cases in both default config serializer and deserializer; unknown types must remain preserved. - `/api/config` is field-by-field parsing, not whole-object Gson mapping. Keep legacy key aliases in sync with new fields. -- Vendored Gson is `2.1`: responses that are `JsonElement` instances must be serialized with `JsonElement.toString()`, not reflective `gson.toJson(Object)`. +- Vendored Gson is `2.1`: HTTP responses that are `JsonElement` instances must be serialized with `JsonElement.toString()`, not reflective `gson.toJson(Object)`; configuration files must use the configured pretty-print Gson path rather than `JsonElement.toString()`. - UI strings use `UIEnv.getRsb()`; add keys to both `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`. +- Web and Swing configuration writes must use `KLALBProxySystem`'s revisioned detached-candidate commit/event path; do not mutate the canonical config object directly. - `KLALBController.PublishedNodeInfo` is the thread-safe source for Tiny/Full node-info responses. Publish name, description, external endpoints, and Extra Routes through the controller method so snapshots and Tiny/Full update flags stay consistent. - `RouterInfo` no longer carries a device name. Its wire format retains an empty legacy UTF slot and `RouterInfoPacket` has optional Tiny/Full invalidation flags. Treat codec changes as compatibility work: preserve old-reader behavior and review a whole-mesh rollout. - Full node-info carries `extraRoutes` separately from the endpoint `data` list. Keep absent fields compatible with older peers. diff --git a/src/klalb_en_US.properties b/src/klalb_en_US.properties index 02a5e59..cf98436 100644 --- a/src/klalb_en_US.properties +++ b/src/klalb_en_US.properties @@ -34,7 +34,13 @@ addline=Add line reconnectall=Reconnect All remotelines=Remote lines settings=Settings -saveconfigsuccess=Save config success +saveconfigsuccess=Save config success +configexternalupdate=The configuration was updated by another source. +configreload=Reload +configcontinue=Continue editing +configsaveconflict=Save failed: the configuration was updated. Reload and try again. +configsavefailed=Failed to save configuration. +configconflicttitle=Configuration conflict warning=Warning invaildipv6addr=IPv6 address:Invaild Input invailddnsserver=DNS server:Invaild Input diff --git a/src/klalb_zh_CN.properties b/src/klalb_zh_CN.properties index fc1b624..ab6b6d5 100644 --- a/src/klalb_zh_CN.properties +++ b/src/klalb_zh_CN.properties @@ -34,7 +34,13 @@ addline=添加链路 reconnectall=全部重连 remotelines=远程链路 settings=设置 -saveconfigsuccess=保存配置成功 +saveconfigsuccess=保存配置成功 +configexternalupdate=配置已被其他来源更新。 +configreload=重新加载 +configcontinue=继续编辑 +configsaveconflict=保存失败:配置已被更新,请重新加载后再试。 +configsavefailed=保存配置失败。 +configconflicttitle=配置冲突 warning=警告 invaildipv6addr=IPv6地址:非法输入 invailddnsserver=DNS服务器:非法输入 diff --git a/src/org/kne/cloud/network/klalb/KLALBController.java b/src/org/kne/cloud/network/klalb/KLALBController.java index ebbbc61..e9b3e1d 100644 --- a/src/org/kne/cloud/network/klalb/KLALBController.java +++ b/src/org/kne/cloud/network/klalb/KLALBController.java @@ -580,7 +580,63 @@ public class KLALBController { private KLALBRoutingProtocolAPIClient apiClient; - private KLALBControllerConfigItem configItem; + private volatile KLALBControllerConfigItem configItem; + + private static List copyConfigList(List values) { + return values == null ? null : new ArrayList(values); + } + + private static KLALBControllerConfigItem copyConfigItem(KLALBControllerConfigItem source) { + KLALBControllerConfigItem copy = new KLALBControllerConfigItem(); + copy.setLanguage(source.getLanguage()); + copy.setNogui(source.isNogui()); + copy.setVirtualAddress(source.getVirtualAddress()); + copy.setVirtualASN(source.getVirtualASN()); + copy.setDNS(copyConfigList(source.getDNS())); + copy.setTCPListen(source.getTCPListen()); + copy.setUDPListen(source.getUDPListen()); + copy.setVirtualSocketName(source.getVirtualSocketName()); + copy.setExternalEndpoints(copyConfigList(source.getExternalEndpoints())); + copy.setAutoConnections(copyConfigList(source.getAutoConnections())); + copy.setNtpServers(copyConfigList(source.getNtpServers())); + copy.setExtraRoutes(copyConfigList(source.getExtraRoutes())); + copy.setDenyExternalEndpointQuery(source.isDenyExternalEndpointQuery()); + copy.setDenyExternalEndpointBroadcast(source.isDenyExternalEndpointBroadcast()); + copy.setCongestionAlgorithm(source.getCongestionAlgorithm()); + copy.setBurstLimit(source.getBurstLimit()); + copy.setDelayUpperBound(source.getDelayUpperBound()); + copy.setDelayLowerBound(source.getDelayLowerBound()); + copy.setNagleDelayTime(source.getNagleDelayTime()); + copy.setLinkNagleDelayTime(source.getLinkNagleDelayTime()); + copy.setLinkConnectionsCount(source.getLinkConnectionsCount()); + copy.setEnableTUN(source.isEnableTUN()); + copy.setTUNName(source.getTUNName()); + copy.setPerformanceStrategy(source.getPerformanceStrategy()); + copy.setDeviceName(source.getDeviceName()); + copy.setDeviceDescription(source.getDeviceDescription()); + copy.setNetworkInterfaceExcepts(copyConfigList(source.getNetworkInterfaceExcepts())); + copy.setWebUI(source.isWebUI()); + copy.setWebListen(source.getWebListen()); + return copy; + } + + public void applyConfigItem(KLALBControllerConfigItem committedConfigItem) { + if (committedConfigItem == null) { + throw new IllegalArgumentException("Controller configuration is required"); + } + KLALBControllerConfigItem detachedConfigItem = copyConfigItem(committedConfigItem); + synchronized (externalEndpoints) { + configItem = detachedConfigItem; + if (srv6Router != null) { + PerformanceStrategy strategy = PerformanceStrategy.fromDescription(detachedConfigItem.getPerformanceStrategy()); + if (strategy != null) { + srv6Router.setPerformanceStrategy(strategy); + } + } + publishNodeInfoLocked(detachedConfigItem.getDeviceName(), detachedConfigItem.getDeviceDescription(), + detachedConfigItem.getExternalEndpoints(), detachedConfigItem.getExtraRoutes()); + } + } public void addRemoteLines(List select) { for (MultiProtocolSocketAddress target : select) { diff --git a/src/org/kne/cloud/network/klalb/KLALBProxySystem.java b/src/org/kne/cloud/network/klalb/KLALBProxySystem.java index 85dda3c..e17d8d6 100644 --- a/src/org/kne/cloud/network/klalb/KLALBProxySystem.java +++ b/src/org/kne/cloud/network/klalb/KLALBProxySystem.java @@ -4,11 +4,21 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.net.InetAddress; +import java.io.Reader; +import java.lang.reflect.Type; +import java.net.InetAddress; import java.net.InetSocketAddress; -import java.util.HashSet; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; import org.kne.cloud.network.*; import org.kne.cloud.network.klalb.ui.KLALBStateGUI3; @@ -19,8 +29,9 @@ import org.kne.cloud.network.klalb.web.KLALBWebServer; import java.util.Set; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; @@ -29,13 +40,82 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.JsonParser; -public class KLALBProxySystem { - private Set proxys=new HashSet<>(); - private KLALBController klalbController; - private KLALBWebServer webServer; - private KLALBConfig config; - private Gson gson; - private File jsonFile; +public class KLALBProxySystem { + public enum ConfigChangeSource { + WEB, SWING + } + + public static final class ControllerConfigSnapshot { + private final long revision; + private final String controllerJson; + + private ControllerConfigSnapshot(long revision, String controllerJson) { + this.revision = revision; + this.controllerJson = controllerJson; + } + + public long getRevision() { + return revision; + } + + public String getControllerJson() { + return controllerJson; + } + } + + public static final class ConfigChangeEvent { + private final ControllerConfigSnapshot snapshot; + private final ConfigChangeSource source; + + private ConfigChangeEvent(ControllerConfigSnapshot snapshot, ConfigChangeSource source) { + this.snapshot = snapshot; + this.source = source; + } + + public ControllerConfigSnapshot getSnapshot() { + return snapshot; + } + + public ConfigChangeSource getSource() { + return source; + } + } + + public static final class CommitResult { + private final boolean success; + private final long revision; + private final ControllerConfigSnapshot snapshot; + + private CommitResult(boolean success, long revision, ControllerConfigSnapshot snapshot) { + this.success = success; + this.revision = revision; + this.snapshot = snapshot; + } + + public boolean isSuccess() { + return success; + } + + public long getRevision() { + return revision; + } + + public ControllerConfigSnapshot getSnapshot() { + return snapshot; + } + } + + private Set proxys=new HashSet<>(); + private KLALBController klalbController; + private KLALBWebServer webServer; + private KLALBConfig config; + private Gson gson; + private File jsonFile; + private final Object configLock = new Object(); + private long configRevision; + private volatile ControllerConfigSnapshot controllerConfigSnapshot = new ControllerConfigSnapshot(0L, null); + private final CopyOnWriteArraySet> configChangeListeners = new CopyOnWriteArraySet<>(); + private JsonArray rawConfigJson; { GsonBuilder gb=new GsonBuilder().setPrettyPrinting(); MultiProtocolSocketAddress.registerToGsonBuilder(gb); @@ -141,13 +221,27 @@ public class KLALBProxySystem { public void loadConfigJson(Reader json) { loadConfigJson(new JsonParser().parse(json)); } - public void loadConfigJson(JsonElement json) { - KLALBConfig config= gson.fromJson(json, KLALBConfig.class); - loadConfig(config); - } - public void loadConfig(KLALBConfig config) { - this.config=config; - for(KLALBConfigItem item:config) { + public void loadConfigJson(JsonElement json) { + JsonElement rawJson = new JsonParser().parse(json.toString()); + KLALBConfig config= gson.fromJson(json, KLALBConfig.class); + JsonArray rawArray = rawJson instanceof JsonArray ? (JsonArray) rawJson : null; + synchronized (configLock) { + installConfigLocked(config, rawArray); + } + } + public void loadConfig(KLALBConfig config) { + JsonElement rawJson = gson.toJsonTree(config); + JsonArray rawArray = rawJson instanceof JsonArray ? (JsonArray) rawJson : null; + synchronized (configLock) { + installConfigLocked(config, rawArray); + } + } + + private void installConfigLocked(KLALBConfig config, JsonArray rawArray) { + this.config=config; + this.configRevision=0L; + rawConfigJson=rawArray; + for(KLALBConfigItem item:config) { if(item instanceof KLALBControllerConfigItem) { KLALBControllerConfigItem kcci=(KLALBControllerConfigItem) item; String lstr=kcci.getLanguage(); @@ -227,41 +321,180 @@ public class KLALBProxySystem { } catch (IOException e) { e.printStackTrace(); } - } - } - } + } + } + controllerConfigSnapshot = createControllerConfigSnapshotLocked(); + } - 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(); - } - } - } + public void saveConfigToFile() { + synchronized (configLock) { + if (jsonFile != null && config != null) { + try { + KLALBControllerConfigItem item = findControllerConfigItemLocked(); + if (item == null) { + persistConfigJsonLocked(gson.toJson(config)); + } else { + String controllerJson = gson.toJson(item); + JsonArray completeConfig = createCompleteConfigJsonLocked(controllerJson); + persistConfigJsonLocked(gson.toJson(completeConfig)); + rawConfigJson = completeConfig; + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private KLALBControllerConfigItem findControllerConfigItemLocked() { + if (config == null) { + return null; + } + for (KLALBConfigItem item : config) { + if (item instanceof KLALBControllerConfigItem) { + return (KLALBControllerConfigItem) item; + } + } + return null; + } + + private int findControllerConfigIndexLocked() { + if (config == null) { + return -1; + } + for (int i = 0; i < config.size(); i++) { + if (config.get(i) instanceof KLALBControllerConfigItem) { + return i; + } + } + return -1; + } + + private ControllerConfigSnapshot createControllerConfigSnapshotLocked() { + KLALBControllerConfigItem item = findControllerConfigItemLocked(); + return new ControllerConfigSnapshot(configRevision, item == null ? null : gson.toJson(item)); + } + + public ControllerConfigSnapshot getControllerConfigSnapshot() { + synchronized (configLock) { + controllerConfigSnapshot = createControllerConfigSnapshotLocked(); + return controllerConfigSnapshot; + } + } + + public KLALBControllerConfigItem parseControllerConfigCandidate(ControllerConfigSnapshot snapshot) { + if (snapshot == null || snapshot.getControllerJson() == null) { + return null; + } + return gson.fromJson(snapshot.getControllerJson(), KLALBControllerConfigItem.class); + } + + private void persistConfigJsonLocked(String json) throws IOException { + if (jsonFile == null) { + return; + } + Path target = jsonFile.toPath().toAbsolutePath(); + Path parent = target.getParent(); + Path temporary = Files.createTempFile(parent, target.getFileName().toString(), ".tmp"); + try { + Files.write(temporary, json.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + try { + Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException e) { + Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING); + } + } finally { + Files.deleteIfExists(temporary); + } + } + + private JsonArray createCompleteConfigJsonLocked(String controllerJson) throws IOException { + JsonElement source = rawConfigJson == null ? gson.toJsonTree(config) : rawConfigJson; + if (!(source instanceof JsonArray)) { + throw new IOException("Configuration is not an array"); + } + JsonArray sourceArray = (JsonArray) source; + int index = findControllerConfigIndexLocked(); + if (index < 0 || index >= sourceArray.size() || sourceArray.size() != config.size()) { + throw new IOException("Configuration item layout changed"); + } + JsonArray mergedConfig = new JsonArray(); + JsonElement controllerElement = new JsonParser().parse(controllerJson); + for (int i = 0; i < sourceArray.size(); i++) { + mergedConfig.add(i == index ? controllerElement : sourceArray.get(i)); + } + return mergedConfig; + } + + public void addConfigChangeListener(Consumer listener) { + configChangeListeners.add(listener); + } + + public void removeConfigChangeListener(Consumer listener) { + configChangeListeners.remove(listener); + } + + public CommitResult commitControllerConfig(long expectedRevision, KLALBControllerConfigItem candidate, + ConfigChangeSource source) throws IOException { + if (candidate == null || source == null) { + throw new IllegalArgumentException("Candidate and source are required"); + } + ConfigChangeEvent event; + CommitResult result; + synchronized (configLock) { + if (expectedRevision != configRevision) { + ControllerConfigSnapshot currentSnapshot = createControllerConfigSnapshotLocked(); + controllerConfigSnapshot = currentSnapshot; + return new CommitResult(false, configRevision, currentSnapshot); + } + int index = findControllerConfigIndexLocked(); + if (index < 0) { + throw new IOException("Controller configuration is missing"); + } + String candidateJson = gson.toJson(candidate); + KLALBControllerConfigItem committedCandidate = gson.fromJson(candidateJson, + KLALBControllerConfigItem.class); + if (PerformanceStrategy.fromDescription(committedCandidate.getPerformanceStrategy()) == null) { + throw new IllegalArgumentException("Unknown performanceStrategy: " + + committedCandidate.getPerformanceStrategy()); + } + com.google.gson.JsonArray mergedConfig = createCompleteConfigJsonLocked(candidateJson); + persistConfigJsonLocked(gson.toJson(mergedConfig)); + rawConfigJson = mergedConfig; + + config.set(index, committedCandidate); + if (klalbController != null) { + klalbController.applyConfigItem(committedCandidate); + } + configRevision++; + controllerConfigSnapshot = createControllerConfigSnapshotLocked(); + event = new ConfigChangeEvent(controllerConfigSnapshot, source); + result = new CommitResult(true, configRevision, controllerConfigSnapshot); + } + for (Consumer listener : configChangeListeners) { + try { + listener.accept(event); + } catch (Throwable e) { + e.printStackTrace(); + } + } + return result; + } private KLALBStateGUI3 kgui; - public KLALBStateGUI3 getKLALBGUI() { - if(kgui==null) { - kgui=new KLALBStateGUI3(klalbController); - kgui.loadConfig(config); - kgui.setSaveComsumer((cfg)->{ - saveConfigToFile(); - }); - } + public KLALBStateGUI3 getKLALBGUI() { + if(kgui==null) { + kgui=new KLALBStateGUI3(klalbController); + kgui.bindConfigSystem(this); + } return kgui; } public KLALBConfig getConfig() { return config; } - public KLALBControllerConfigItem getControllerConfig() { - for(KLALBConfigItem item:config) { - if(item instanceof KLALBControllerConfigItem) - return (KLALBControllerConfigItem) item; - } - return null; - } -} + public KLALBControllerConfigItem getControllerConfig() { + return parseControllerConfigCandidate(getControllerConfigSnapshot()); + } +} diff --git a/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java b/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java index b110b94..c72a353 100644 --- a/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java +++ b/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java @@ -104,8 +104,82 @@ public class KLALBStateGUI3 extends XFrame { private JCheckBox nogui; // ==================== 配置和回调 ==================== - private KLALBConfig config; // 配置文件 - private Consumer saveComsumer; // 保存配置的回调函数 + private KLALBConfig config; // 配置文件 + private Consumer saveComsumer; // 保存配置的回调函数 + private KLALBProxySystem configSystem; + private KLALBProxySystem.ControllerConfigSnapshot loadedSnapshot; + private long loadedRevision; + private SettingsFormState loadedFormState; + private KLALBProxySystem.ControllerConfigSnapshot pendingExternalSnapshot; + private long pendingExternalRevision; + private boolean configSystemClosed; + private long bindingGeneration; + private Consumer boundConfigChangeListener; + + private static final class SettingsFormState { + private final String language, deviceName, deviceDescription, address, dns, extraRoutes; + private final String asn, tunName, webListen, tcpListen, udpListen, openLines, connectLines, ntp; + private final String performance, congestion; + private final boolean enableTun, webApi, nogui, denyQuery, denyBroadcast; + private final List interfaces; + private final int connections, burst, upper, lower, nagle, linkNagle; + + private SettingsFormState(KLALBStateGUI3 gui) { + Language languageItem = (Language) gui.comboLang.getSelectedItem(); + language = languageItem == null ? null : languageItem.name(); + deviceName = gui.deviceNameSet.getText(); + deviceDescription = gui.deviceDescriptionSet.getText(); + address = gui.addressFieldSet.getText(); + dns = gui.dnsAreaSet.getText(); + extraRoutes = gui.extraRoutesSet.getText(); + asn = gui.asnFieldSet.getText(); + tunName = gui.tunDeviceName.getText(); + webListen = gui.webListenSet.getText(); + tcpListen = gui.tcpListeningSet.getText(); + udpListen = gui.udpListeningSet.getText(); + openLines = gui.openLineTabelSet.getText(); + connectLines = gui.connectLineTabelSet.getText(); + ntp = gui.ntpServerSet.getText(); + PerformanceStrategyItem performanceItem = (PerformanceStrategyItem) gui.comboPerformance.getSelectedItem(); + performance = performanceItem == null ? null : performanceItem.getStrategy().toString(); + congestion = String.valueOf(gui.congestions.getComboBox().getSelectedItem()); + enableTun = gui.enableTUN.isSelected(); + webApi = gui.webApiEnabled.isSelected(); + nogui = gui.nogui.isSelected(); + denyQuery = gui.denyQuery.isSelected(); + denyBroadcast = gui.denyBroadcast.isSelected(); + interfaces = new ArrayList<>(); + for (int i = 0; i < gui.nilsimdl.getSize(); i++) interfaces.add(gui.nilsimdl.getElementAt(i).getName()); + connections = gui.linkConnectionsCount.getSlider().getValue(); + burst = gui.burstLimit.getSlider().getValue(); + upper = gui.delayHbound.getSlider().getValue(); + lower = gui.delayLbound.getSlider().getValue(); + nagle = gui.nagleDelayTime.getSlider().getValue(); + linkNagle = gui.linkNagleDelayTime.getSlider().getValue(); + } + + @Override public boolean equals(Object obj) { + if (!(obj instanceof SettingsFormState)) return false; + SettingsFormState o = (SettingsFormState) obj; + return enableTun == o.enableTun && webApi == o.webApi && nogui == o.nogui + && denyQuery == o.denyQuery && denyBroadcast == o.denyBroadcast + && connections == o.connections && burst == o.burst && upper == o.upper + && lower == o.lower && nagle == o.nagle && linkNagle == o.linkNagle + && Objects.equals(language, o.language) && Objects.equals(deviceName, o.deviceName) + && Objects.equals(deviceDescription, o.deviceDescription) && Objects.equals(address, o.address) + && Objects.equals(dns, o.dns) && Objects.equals(extraRoutes, o.extraRoutes) + && Objects.equals(asn, o.asn) && Objects.equals(tunName, o.tunName) + && Objects.equals(webListen, o.webListen) && Objects.equals(tcpListen, o.tcpListen) + && Objects.equals(udpListen, o.udpListen) && Objects.equals(openLines, o.openLines) + && Objects.equals(connectLines, o.connectLines) && Objects.equals(ntp, o.ntp) + && Objects.equals(performance, o.performance) && Objects.equals(congestion, o.congestion) + && Objects.equals(interfaces, o.interfaces); + } + @Override public int hashCode() { return Objects.hash(language, deviceName, deviceDescription, address, dns, + extraRoutes, asn, tunName, webListen, tcpListen, udpListen, openLines, connectLines, ntp, + performance, congestion, enableTun, webApi, nogui, denyQuery, denyBroadcast, interfaces, + connections, burst, upper, lower, nagle, linkNagle); } + } // ==================== 尺寸常量 ==================== private Dimension dashSize = new Dimension((int) (145 * 0.7), (int) (165 * 0.7)); // 仪表盘尺寸 @@ -1055,15 +1129,10 @@ public class KLALBStateGUI3 extends XFrame { /** * 保存配置到文件 */ - private void saveConfig() { - if (config == null) { - config = new KLALBConfig(); - config.add(new KLALBControllerConfigItem()); - } - - for (KLALBConfigItem item : config) { - if (item instanceof KLALBControllerConfigItem) { - KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item; + private void saveConfig() { + if (configSystem == null || loadedSnapshot == null) return; + KLALBControllerConfigItem kck = configSystem.parseControllerConfigCandidate(loadedSnapshot); + if (kck == null) return; String oldDeviceName = kck.getDeviceName(); String oldDeviceDescription = kck.getDeviceDescription(); List oldExternalEndpoints = kck.getExternalEndpoints() == null @@ -1314,30 +1383,32 @@ public class KLALBStateGUI3 extends XFrame { kck.setLinkNagleDelayTime(linkNagleDelayTime.getSlider().getValue()*100000L); - boolean deviceNameChanged = !Objects.equals(oldDeviceName, newDeviceName); - boolean deviceDescriptionChanged = !Objects.equals(oldDeviceDescription, newDeviceDescription); - boolean externalEndpointsChanged = !Objects.equals(oldExternalEndpoints, newExternalEndpoints); - boolean extraRoutesChanged = !Objects.equals(oldExtraRoutes, newExtraRoutes); - if (deviceNameChanged || deviceDescriptionChanged || externalEndpointsChanged || extraRoutesChanged) { - if (kcontroller != null) { - kcontroller.publishNodeInfo(newDeviceName, newDeviceDescription, newExternalEndpoints, - newExtraRoutes); - } else { - kck.setDeviceName(newDeviceName); - kck.setDeviceDescription(newDeviceDescription); - kck.setExternalEndpoints(newExternalEndpoints); - kck.setExtraRoutes(newExtraRoutes); - } - } - } - } - - // 调用保存回调 - if (saveComsumer != null) { - saveComsumer.accept(config); - JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("saveconfigsuccess")); - } - } + kck.setDeviceName(newDeviceName); + kck.setDeviceDescription(newDeviceDescription); + kck.setExternalEndpoints(newExternalEndpoints); + kck.setExtraRoutes(newExtraRoutes); + try { + KLALBProxySystem.CommitResult result = configSystem.commitControllerConfig(loadedRevision, kck, + KLALBProxySystem.ConfigChangeSource.SWING); + if (!result.isSuccess()) { + resolveConfigConflict(result.getSnapshot(), "configsaveconflict"); + return; + } + loadedSnapshot = result.getSnapshot(); + loadedRevision = result.getRevision(); + loadedFormState = new SettingsFormState(this); + pendingExternalSnapshot = null; + JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("saveconfigsuccess")); + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("configsavefailed"), + UIEnv.getRsb().getString("warning"), JOptionPane.WARNING_MESSAGE); + } catch (RuntimeException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("configsavefailed"), + UIEnv.getRsb().getString("warning"), JOptionPane.WARNING_MESSAGE); + } + } // ==================== 刷新任务创建 ==================== /** @@ -1565,11 +1636,7 @@ public class KLALBStateGUI3 extends XFrame { /** * 加载配置文件 */ - public void loadConfig(KLALBConfig config) { - this.config = config; - for (KLALBConfigItem item : config) { - if (item instanceof KLALBControllerConfigItem) { - KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item; + private void loadConfigCandidate(KLALBControllerConfigItem kck) { // 加载语言设置 String lg = kck.getLanguage(); @@ -1695,9 +1762,92 @@ public class KLALBStateGUI3 extends XFrame { long delr=kck.getLinkNagleDelayTime(); linkNagleDelayTime.getSlider().setValue((int)(delr/100000L)); - } - } - } + } + + public void loadConfig(KLALBConfig config) { + if (configSystem != null) return; + this.config = config; + if (config == null) return; + for (KLALBConfigItem item : config) { + if (item instanceof KLALBControllerConfigItem) { + loadConfigCandidate((KLALBControllerConfigItem) item); + break; + } + } + } + + public void bindConfigSystem(KLALBProxySystem system) { + if (configSystemClosed || system == null) return; + final long generation = ++bindingGeneration; + Runnable bind = () -> { + if (configSystemClosed || generation != bindingGeneration) return; + if (configSystem != null && boundConfigChangeListener != null) + configSystem.removeConfigChangeListener(boundConfigChangeListener); + configSystem = system; + final Consumer listener = event -> + SwingUtilities.invokeLater(() -> handleConfigChange(system, generation, event)); + boundConfigChangeListener = listener; + pendingExternalSnapshot = null; + pendingExternalRevision = 0L; + loadedSnapshot = null; + loadedRevision = 0L; + loadedFormState = null; + system.addConfigChangeListener(listener); + loadedSnapshot = system.getControllerConfigSnapshot(); + KLALBControllerConfigItem candidate = system.parseControllerConfigCandidate(loadedSnapshot); + if (candidate != null) { + loadConfigCandidate(candidate); + loadedRevision = loadedSnapshot.getRevision(); + loadedFormState = new SettingsFormState(this); + } + }; + if (SwingUtilities.isEventDispatchThread()) bind.run(); + else SwingUtilities.invokeLater(bind); + } + + private void handleConfigChange(KLALBProxySystem sourceSystem, long generation, + KLALBProxySystem.ConfigChangeEvent event) { + if (configSystemClosed || generation != bindingGeneration || sourceSystem != configSystem + || event.getSource() == KLALBProxySystem.ConfigChangeSource.SWING) return; + KLALBProxySystem.ControllerConfigSnapshot snapshot = event.getSnapshot(); + if (snapshot == null || snapshot.getRevision() <= Math.max(loadedRevision, pendingExternalRevision)) return; + if (loadedFormState != null && loadedFormState.equals(new SettingsFormState(this))) { + applyExternalSnapshot(snapshot); + return; + } + if (pendingExternalSnapshot == null || snapshot.getRevision() > pendingExternalRevision) { + boolean hadPending = pendingExternalSnapshot != null; + pendingExternalSnapshot = snapshot; + pendingExternalRevision = snapshot.getRevision(); + if (!hadPending) resolveConfigConflict(snapshot, "configexternalupdate"); + } + } + + private void resolveConfigConflict(KLALBProxySystem.ControllerConfigSnapshot snapshot, String messageKey) { + if (snapshot != null && snapshot.getRevision() > Math.max(loadedRevision, pendingExternalRevision)) { + pendingExternalSnapshot = snapshot; + pendingExternalRevision = snapshot.getRevision(); + } + Object[] options = { UIEnv.getRsb().getString("configreload"), UIEnv.getRsb().getString("configcontinue") }; + int choice = JOptionPane.showOptionDialog(this, UIEnv.getRsb().getString(messageKey), + UIEnv.getRsb().getString("configconflicttitle"), JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, null, options, options[0]); + if (choice == 0 && pendingExternalSnapshot != null + && pendingExternalSnapshot.getRevision() >= loadedRevision) { + applyExternalSnapshot(pendingExternalSnapshot); + } + } + + private void applyExternalSnapshot(KLALBProxySystem.ControllerConfigSnapshot snapshot) { + KLALBControllerConfigItem candidate = configSystem.parseControllerConfigCandidate(snapshot); + if (candidate == null) return; + loadConfigCandidate(candidate); + loadedSnapshot = snapshot; + loadedRevision = snapshot.getRevision(); + loadedFormState = new SettingsFormState(this); + pendingExternalSnapshot = null; + pendingExternalRevision = 0L; + } // ==================== 辅助方法 ==================== /** @@ -1772,8 +1922,9 @@ public class KLALBStateGUI3 extends XFrame { /** * 设置保存配置的回调函数 */ - public void setSaveComsumer(Consumer saveComsumer) { - this.saveComsumer = saveComsumer; + public void setSaveComsumer(Consumer saveComsumer) { + if (configSystem != null) return; + this.saveComsumer = saveComsumer; } /** @@ -1788,6 +1939,11 @@ public class KLALBStateGUI3 extends XFrame { * 关闭窗口并清理资源 */ public void close() { + bindingGeneration++; + configSystemClosed = true; + if (configSystem != null && boundConfigChangeListener != null) + configSystem.removeConfigChangeListener(boundConfigChangeListener); + boundConfigChangeListener = null; for (int i = 0; i < tabbedPane.getTabCount(); i++) { Component component = tabbedPane.getComponentAt(i); if (component instanceof NodeInformationPanel) { diff --git a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java index 2a5dd59..430f57d 100644 --- a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java +++ b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java @@ -869,17 +869,11 @@ public class KLALBWebServer { String body = readRequestBody(exchange); try { JsonObject json = new JsonParser().parse(body).getAsJsonObject(); - KLALBControllerConfigItem current = proxySystem.getControllerConfig(); + KLALBProxySystem.ControllerConfigSnapshot baseSnapshot = proxySystem.getControllerConfigSnapshot(); + KLALBControllerConfigItem current = proxySystem.parseControllerConfigCandidate(baseSnapshot); if (current != null) { - String oldDeviceName = current.getDeviceName(); - String oldDeviceDescription = current.getDeviceDescription(); - List oldExternalEndpoints = current.getExternalEndpoints() == null - ? null - : new ArrayList(current.getExternalEndpoints()); - List oldExtraRoutes = current.getExtraRoutes() == null - ? null : new ArrayList(current.getExtraRoutes()); - String newDeviceName = oldDeviceName; - String newDeviceDescription = oldDeviceDescription; + String newDeviceName = current.getDeviceName(); + String newDeviceDescription = current.getDeviceDescription(); List newExternalEndpoints = current.getExternalEndpoints(); List newExtraRoutes = current.getExtraRoutes(); if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) { @@ -1078,37 +1072,32 @@ public class KLALBWebServer { current.setDenyExternalEndpointBroadcast(json.get("denyLineTableBroadcast").getAsBoolean()); } - boolean deviceNameChanged = !Objects.equals(oldDeviceName, newDeviceName); - boolean deviceDescriptionChanged = !Objects.equals(oldDeviceDescription, newDeviceDescription); - boolean externalEndpointsChanged = !Objects.equals(oldExternalEndpoints, newExternalEndpoints); - boolean extraRoutesChanged = !Objects.equals(oldExtraRoutes, newExtraRoutes); - if (deviceNameChanged || deviceDescriptionChanged || externalEndpointsChanged || extraRoutesChanged) { - KLALBController kc = proxySystem.getKlalbController(); - if (kc != null) { - kc.publishNodeInfo(newDeviceName, newDeviceDescription, newExternalEndpoints, - newExtraRoutes); - } else { - current.setDeviceName(newDeviceName); - current.setDeviceDescription(newDeviceDescription); - current.setExternalEndpoints(newExternalEndpoints); - current.setExtraRoutes(newExtraRoutes); - } - } - - // Trigger GUI save consumer or save directly - if (proxySystem.getKLALBGUI() != null && proxySystem.getKLALBGUI().getSaveComsumer() != null) { - proxySystem.getKLALBGUI().getSaveComsumer().accept(proxySystem.getConfig()); - } else { - proxySystem.saveConfigToFile(); + current.setDeviceName(newDeviceName); + current.setDeviceDescription(newDeviceDescription); + current.setExternalEndpoints(newExternalEndpoints); + current.setExtraRoutes(newExtraRoutes); + KLALBProxySystem.CommitResult commitResult = proxySystem.commitControllerConfig( + baseSnapshot.getRevision(), current, + KLALBProxySystem.ConfigChangeSource.WEB); + if (!commitResult.isSuccess()) { + JsonObject conflict = new JsonObject(); + conflict.addProperty("success", false); + conflict.addProperty("revision", commitResult.getRevision()); + conflict.addProperty("error", "Configuration revision conflict"); + sendJsonResponse(exchange, 409, conflict); + return; } JsonObject resp = new JsonObject(); resp.addProperty("success", true); + resp.addProperty("revision", commitResult.getRevision()); resp.addProperty("message", "Configuration updated successfully"); sendJsonResponse(exchange, 200, resp); } else { sendError(exchange, 500, "Current configuration is null"); } + } catch (IOException e) { + sendError(exchange, 500, "Failed to persist configuration: " + e.getMessage()); } catch (Exception e) { sendError(exchange, 400, "Failed to update configuration: " + e.getMessage()); } -- 2.39.5 From 3dae7dd876c6b2a4063aea5fd9e5515d2983069e Mon Sep 17 00:00:00 2001 From: SerinaNya <34389622+SerinaNya@users.noreply.github.com> Date: Mon, 31 Aug 2026 01:13:49 +0800 Subject: [PATCH 3/4] feat(srv6)!: split node information queries Replace the combined node-info protocol with independent profile, external-endpoint, and extra-route requests. Aggregate consumers use NodeInfoQueryCoordinator and validate response source addresses. BREAKING CHANGE: nodeinfotinyreq/resp and nodeinfofullreq/resp are removed. Peers must use the profile, external-endpoint, and extra-route request pairs. --- AGENTS.md | 43 +-- dashboard | 2 +- .../cloud/network/klalb/KLALBController.java | 17 +- .../klalb/NodeInfoQueryCoordinator.java | 211 +++++++++++ .../network/klalb/ui/NetworkGraphPanel.java | 22 +- .../klalb/ui/NodeInformationPanel.java | 95 +++-- .../network/klalb/web/KLALBWebServer.java | 62 ++-- .../network/srv6/ExternalEndpointsResult.java | 49 +++ .../cloud/network/srv6/ExtraRoutesResult.java | 47 +++ .../network/srv6/KLALBNodeInformation.java | 68 ---- .../srv6/KLALBRoutingProtocolAPIClient.java | 333 +++++++++++------- .../srv6/KLALBRoutingProtocolAPIServer.java | 181 +++++----- .../srv6/KLALBRoutingProtocolJsonData.java | 184 +++++----- .../network/srv6/NodeInfoQueryStatus.java | 6 + .../kne/cloud/network/srv6/NodeProfile.java | 44 +++ 15 files changed, 878 insertions(+), 486 deletions(-) create mode 100644 src/org/kne/cloud/network/klalb/NodeInfoQueryCoordinator.java create mode 100644 src/org/kne/cloud/network/srv6/ExternalEndpointsResult.java create mode 100644 src/org/kne/cloud/network/srv6/ExtraRoutesResult.java delete mode 100644 src/org/kne/cloud/network/srv6/KLALBNodeInformation.java create mode 100644 src/org/kne/cloud/network/srv6/NodeInfoQueryStatus.java create mode 100644 src/org/kne/cloud/network/srv6/NodeProfile.java diff --git a/AGENTS.md b/AGENTS.md index c323de6..56cdb69 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,54 +1,39 @@ # KLALB Repository Guide -KLALB is a Java SRv6/load-balancing system. The executable entrypoint is `org.kne.cloud.network.klalb.KLALBMain`; runtime configuration is `klalb-config.json` in the repository root. +KLALB is a Java SRv6/load-balancing system. Start at `org.kne.cloud.network.klalb.KLALBMain`; runtime configuration is root-relative `klalb-config.json`. ## Build And Run -- This is a plain Eclipse/IntelliJ Java project: sources are `src/`, vendored dependencies are `lib/`, and output is `bin/`. There is no Maven or Gradle. -- `.classpath` targets `JavaSE-25`. Sources contain Chinese text, so manual compilation must use UTF-8: +- This is an Eclipse Java project, not Maven or Gradle: `src/` contains sources, `lib/` vendored dependencies, and `bin/` compiled output. `.classpath` targets Java 25. +- Source files contain Chinese text, so compile with UTF-8 from the repository root: ```powershell & "C:\Program Files\Zulu\zulu-25\bin\javac.exe" -encoding UTF-8 -cp "lib/*" -d bin (Get-ChildItem -Recurse src -Filter *.java | ForEach-Object FullName) ``` -- Run from the repository root. Manual `javac` does not copy resources, so keep `src` on the runtime classpath: +- Run from the repository root. `src` must stay on the classpath because manual compilation does not copy resource bundles: ```powershell & "C:\Program Files\Zulu\zulu-25\bin\java.exe" --enable-native-access=ALL-UNNAMED "--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED" -cp "bin;src;lib/*" org.kne.cloud.network.klalb.KLALBMain ``` -- The native libraries and `klalb-config.json` are resolved from the current directory. Restart a running JVM after recompiling. -- TUN creation normally needs elevation. For non-admin UI/routing checks, set `"enableTUN": false`. -- Current full compilation emits 11 pre-existing varargs/deprecation warnings; exit code `0` is success. +- Native libraries and `klalb-config.json` are resolved from the current directory; restart the JVM after recompiling. TUN creation requires elevation, so use `"enableTUN": false` for non-admin checks. ## Dashboard -- `dashboard/` is a Git submodule. Commit dashboard changes inside it, then update the parent repository's submodule pointer. -- Run frontend commands from `dashboard/` with pnpm: - -```powershell -pnpm install --frozen-lockfile -pnpm lint -pnpm typecheck -pnpm build -pnpm dev -``` - -- `pnpm build` runs `tsc -b` then Vite and writes `dashboard/dist`, which the Java web server hosts. Vite development proxies `/api` to `http://127.0.0.1:4665`. -- Add shadcn components through `pnpm dlx shadcn@latest add `; do not hand-create replacements for installed shadcn primitives. +- `dashboard/` is a Git submodule. Commit dashboard changes in that repository, then update the parent submodule pointer. +- Run frontend commands from `dashboard/`: `pnpm install --frozen-lockfile`, `pnpm lint`, `pnpm typecheck`, and `pnpm build`. The build is `tsc -b && vite build` and produces `dashboard/dist`, which the Java server hosts. +- Vite proxies `/api` to `http://127.0.0.1:4665`; use `pnpm dev` only with the Java API running there. ## Verification -- There is no CI or automated test suite. `*Test*` classes are manual harnesses that require real network peers. -- For Java changes, compile and launch the app. For dashboard changes, run `pnpm typecheck` and `pnpm build`. +- No repository test runner or CI workflow is configured. For Java changes, compile and launch the app; for dashboard changes, run `pnpm typecheck` and `pnpm build`. ## Important Boundaries -- `KLALBConfigItem` is a polymorphic JSON array keyed by `Type`. Adding a type requires a subclass and cases in both default config serializer and deserializer; unknown types must remain preserved. -- `/api/config` is field-by-field parsing, not whole-object Gson mapping. Keep legacy key aliases in sync with new fields. -- Vendored Gson is `2.1`: HTTP responses that are `JsonElement` instances must be serialized with `JsonElement.toString()`, not reflective `gson.toJson(Object)`; configuration files must use the configured pretty-print Gson path rather than `JsonElement.toString()`. +- `KLALBConfigItem` is a polymorphic JSON array keyed by case-sensitive `Type`. New types need serializer and deserializer support; preserve unknown items' raw JSON. +- `/api/config` parses fields and legacy aliases explicitly. Web and Swing writes must use `KLALBProxySystem`'s revision-checked detached-candidate commit path, never mutate the canonical config directly. +- Vendored Gson is 2.1: serialize HTTP `JsonElement` values with `toString()`; serialize configuration through the configured pretty-print Gson path. - UI strings use `UIEnv.getRsb()`; add keys to both `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`. -- Web and Swing configuration writes must use `KLALBProxySystem`'s revisioned detached-candidate commit/event path; do not mutate the canonical config object directly. -- `KLALBController.PublishedNodeInfo` is the thread-safe source for Tiny/Full node-info responses. Publish name, description, external endpoints, and Extra Routes through the controller method so snapshots and Tiny/Full update flags stay consistent. -- `RouterInfo` no longer carries a device name. Its wire format retains an empty legacy UTF slot and `RouterInfoPacket` has optional Tiny/Full invalidation flags. Treat codec changes as compatibility work: preserve old-reader behavior and review a whole-mesh rollout. -- Full node-info carries `extraRoutes` separately from the endpoint `data` list. Keep absent fields compatible with older peers. +- `KLALBController.PublishedNodeInfo` is the thread-safe node-info source. Publish profile, effective external endpoints, and extra routes through the controller so snapshots and update flags remain coherent. Peer queries are separate profile, endpoint, and extra-route requests; use `NodeInfoQueryCoordinator` when a consumer needs an aggregate detail result. +- `RouterInfo` retains an empty legacy UTF slot. `RouterInfoPacket` appends optional update flags behind a marker; treat binary codec changes as mesh-compatibility work and preserve old-reader behavior. diff --git a/dashboard b/dashboard index 0b705f9..8c13af6 160000 --- a/dashboard +++ b/dashboard @@ -1 +1 @@ -Subproject commit 0b705f9ba0008540099e307bbb1e3e6944d7aa71 +Subproject commit 8c13af680a88f38abc033144e1c273b9ab3e2117 diff --git a/src/org/kne/cloud/network/klalb/KLALBController.java b/src/org/kne/cloud/network/klalb/KLALBController.java index e9b3e1d..cd1460a 100644 --- a/src/org/kne/cloud/network/klalb/KLALBController.java +++ b/src/org/kne/cloud/network/klalb/KLALBController.java @@ -53,10 +53,11 @@ import org.kne.cloud.network.ntp.NTPContext; import org.kne.cloud.network.ntp.NTPv4Packet; import org.kne.cloud.network.ntp.NTPv4Protocol; import org.kne.cloud.network.ntp.NTPv4Protocol.NTPPeer; -import org.kne.cloud.network.srv6.KLALBRoutingProtocol; -import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; -import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIServer; -import org.kne.cloud.network.srv6.SRv6Router; +import org.kne.cloud.network.srv6.KLALBRoutingProtocol; +import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; +import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIServer; +import org.kne.cloud.network.srv6.NodeInfoQueryStatus; +import org.kne.cloud.network.srv6.SRv6Router; import org.kne.cloud.network.srv6.SRv6RouterListener; import org.kne.cloud.network.tcp.UDPPacket; import org.kne.cloud.network.tcp.UDPProtocolRegister; @@ -389,9 +390,11 @@ public class KLALBController { try { InetSocketAddress iaddr = new InetSocketAddress(neighbor.getAddress().toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT); - apiClient.requestNodeInfoFull(iaddr, (v) -> { - ThreadTool.makeVDaemonThreadIfSupport("线路添加任务", () -> { - for (MultiProtocolSocketAddress msa : v.getOpenLines()) { + apiClient.requestExternalEndpoints(iaddr, (v) -> { + if (v == null || v.getStatus() != NodeInfoQueryStatus.OK) return; + ThreadTool.makeVDaemonThreadIfSupport("线路添加任务", () -> { + if (v.getExternalEndpoints() == null) return; + for (MultiProtocolSocketAddress msa : v.getExternalEndpoints()) { // System.out.print(msa); addRemoteLines(msa); } diff --git a/src/org/kne/cloud/network/klalb/NodeInfoQueryCoordinator.java b/src/org/kne/cloud/network/klalb/NodeInfoQueryCoordinator.java new file mode 100644 index 0000000..e9d4ed2 --- /dev/null +++ b/src/org/kne/cloud/network/klalb/NodeInfoQueryCoordinator.java @@ -0,0 +1,211 @@ +package org.kne.cloud.network.klalb; + +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.kne.cloud.network.MultiProtocolSocketAddress; +import org.kne.cloud.network.srv6.ExternalEndpointsResult; +import org.kne.cloud.network.srv6.ExtraRoutesResult; +import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; +import org.kne.cloud.network.srv6.NodeInfoQueryStatus; +import org.kne.cloud.network.srv6.NodeProfile; + +/** Coordinates the three independent sections of a remote node-info query. */ +public final class NodeInfoQueryCoordinator { + private static final long DEADLINE_MILLIS = 3000L; + private static final ExecutorService REQUEST_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); + private static final ScheduledExecutorService DEADLINE_EXECUTOR = Executors + .newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r, "KLALB-node-info-deadline"); + thread.setDaemon(true); + return thread; + }); + + private final KLALBRoutingProtocolAPIClient client; + + public NodeInfoQueryCoordinator(KLALBRoutingProtocolAPIClient client) { + this.client = Objects.requireNonNull(client, "client"); + } + + /** Starts all section requests and completes at the first all-sections response or the shared deadline. */ + public CompletableFuture query(SocketAddress address) { + Objects.requireNonNull(address, "address"); + Pending pending = new Pending(); + pending.result.whenComplete((ignored, error) -> { + if (pending.result.isCancelled()) pending.cancel(); + }); + pending.deadline = DEADLINE_EXECUTOR.schedule(pending::timeout, DEADLINE_MILLIS, TimeUnit.MILLISECONDS); + + issue(pending, () -> client.requestNodeProfile(address, pending::profile), pending::profileFailed); + issue(pending, () -> client.requestExternalEndpoints(address, pending::externalEndpoints), + pending::externalEndpointsFailed); + issue(pending, () -> client.requestExtraRoutes(address, pending::extraRoutes), pending::extraRoutesFailed); + return pending.result; + } + + private void issue(Pending pending, ThrowingRequest request, Runnable failure) { + REQUEST_EXECUTOR.execute(() -> pending.issue(request, failure)); + } + + @FunctionalInterface + private interface ThrowingRequest { + void run() throws Exception; + } + + public static final class Section { + private final boolean received; + private final Optional value; + private final Optional status; + + private Section(boolean received, T value, NodeInfoQueryStatus status) { + this.received = received; + this.value = Optional.ofNullable(value); + this.status = Optional.ofNullable(status); + } + + public boolean isReceived() { + return received; + } + + public Optional getValue() { + return value; + } + + public Optional getStatus() { + return status; + } + + private static Section missing() { + return new Section(false, null, null); + } + + private static Section received(T value, NodeInfoQueryStatus status) { + return new Section(true, value, status); + } + } + + public static final class Result { + private final Section profile; + private final Section> externalEndpoints; + private final Section> extraRoutes; + + private Result(Section profile, + Section> externalEndpoints, + Section> extraRoutes) { + this.profile = profile; + this.externalEndpoints = externalEndpoints; + this.extraRoutes = extraRoutes; + } + + public Section getProfile() { + return profile; + } + + public Section> getExternalEndpoints() { + return externalEndpoints; + } + + public Section> getExtraRoutes() { + return extraRoutes; + } + } + + private final class Pending { + private final CompletableFuture result = new CompletableFuture<>(); + private final AtomicBoolean finished = new AtomicBoolean(); + private int remaining = 3; + private Section profile = Section.missing(); + private Section> externalEndpoints = Section.missing(); + private Section> extraRoutes = Section.missing(); + private java.util.concurrent.ScheduledFuture deadline; + + private synchronized void profile(NodeProfile value) { + if (profile.isReceived()) return; + profile = Section.received(value, NodeInfoQueryStatus.OK); + completeSection(); + } + + private synchronized void profileFailed() { + if (profile.isReceived()) return; + profile = Section.missing(); + completeSection(); + } + + private synchronized void externalEndpoints(ExternalEndpointsResult value) { + if (externalEndpoints.isReceived()) return; + if (value == null) { + externalEndpoints = Section.missing(); + } else { + List endpoints = value.getExternalEndpoints(); + externalEndpoints = Section.received(endpoints == null + ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList(endpoints)), + value.getStatus()); + } + completeSection(); + } + + private synchronized void externalEndpointsFailed() { + if (externalEndpoints.isReceived()) return; + externalEndpoints = Section.missing(); + completeSection(); + } + + private synchronized void extraRoutes(ExtraRoutesResult value) { + if (extraRoutes.isReceived()) return; + if (value == null) { + extraRoutes = Section.missing(); + } else { + List routes = value.getExtraRoutes(); + extraRoutes = Section.received(routes == null + ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList(routes)), value.getStatus()); + } + completeSection(); + } + + private synchronized void extraRoutesFailed() { + if (extraRoutes.isReceived()) return; + extraRoutes = Section.missing(); + completeSection(); + } + + private synchronized void issue(ThrowingRequest request, Runnable failure) { + if (finished.get() || result.isDone() || result.isCancelled()) return; + try { + request.run(); + } catch (Exception e) { + failure.run(); + } + } + + private synchronized void cancel() { + finish(); + } + + private void completeSection() { + remaining--; + if (remaining == 0) finish(); + } + + private synchronized void timeout() { + finish(); + } + + private void finish() { + if (!finished.compareAndSet(false, true)) return; + if (deadline != null) deadline.cancel(false); + result.complete(new Result(profile, externalEndpoints, extraRoutes)); + } + } +} diff --git a/src/org/kne/cloud/network/klalb/ui/NetworkGraphPanel.java b/src/org/kne/cloud/network/klalb/ui/NetworkGraphPanel.java index eb832cb..6b0de51 100644 --- a/src/org/kne/cloud/network/klalb/ui/NetworkGraphPanel.java +++ b/src/org/kne/cloud/network/klalb/ui/NetworkGraphPanel.java @@ -22,13 +22,13 @@ import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; -import org.kne.cloud.network.MultiProtocolSocketAddress; -import org.kne.cloud.network.ipv6.IPv6Address; -import org.kne.cloud.network.klalb.KLALBController; -import org.kne.cloud.network.srv6.KLALBNodeInformation; +import org.kne.cloud.network.MultiProtocolSocketAddress; +import org.kne.cloud.network.ipv6.IPv6Address; +import org.kne.cloud.network.klalb.KLALBController; import org.kne.cloud.network.srv6.KLALBRoutingProtocol; import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection; +import org.kne.cloud.network.srv6.NodeProfile; import org.kne.cloud.network.srv6.RouterInfoPacket; public class NetworkGraphPanel extends GraphPanel { @@ -205,7 +205,7 @@ public class NetworkGraphPanel extends GraphPanel { } } - private void requestNodeInfoTiny(final IPv6Address address) { + private void requestNodeProfile(final IPv6Address address) { final long requestTime = System.currentTimeMillis(); final long requestGeneration; synchronized (nodeInfoLock) { @@ -225,9 +225,9 @@ public class NetworkGraphPanel extends GraphPanel { nodeInfoRequestGenerations.put(address, requestGeneration); } try { - nodeInfoClient.requestNodeInfoTiny( + nodeInfoClient.requestNodeProfile( new InetSocketAddress(address.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), - (info) -> SwingUtilities.invokeLater(() -> handleNodeInfo(address, requestGeneration, info))); + (profile) -> SwingUtilities.invokeLater(() -> handleNodeProfile(address, requestGeneration, profile))); } catch (IOException e) { synchronized (nodeInfoLock) { if (nodeInfoRequestGenerations.get(address) != null @@ -239,12 +239,12 @@ public class NetworkGraphPanel extends GraphPanel { } } - private void handleNodeInfo(IPv6Address address, long requestGeneration, KLALBNodeInformation info) { + private void handleNodeProfile(IPv6Address address, long requestGeneration, NodeProfile profile) { if (nodeInfoClosed) return; boolean currentRequest; boolean nodeExists = getNodes().containsKey(address); - String deviceName = info == null ? "" : info.getDeviceName(); + String deviceName = profile == null ? "" : profile.getDeviceName(); if (deviceName == null || deviceName.isEmpty()) deviceName = ""; synchronized (nodeInfoLock) { @@ -295,7 +295,7 @@ public class NetworkGraphPanel extends GraphPanel { node.updateLabel(); } repaint(); - requestNodeInfoTiny(address); + requestNodeProfile(address); } public void close() { @@ -349,7 +349,7 @@ public class NetworkGraphPanel extends GraphPanel { getNodes().put(inet6Address,new InetGraphNode(inet6Address,Color.BLACK,v2pos.x,v2pos.y,inet6Address.equals(controller.getIpv6Router().getLocator().getAddress()))); } if(!inet6Address.equals(localAddress)) - requestNodeInfoTiny(inet6Address); + requestNodeProfile(inet6Address); } Set kns=getNodes().keySet(); for (Iterator iterator = kns.iterator(); iterator.hasNext();) { diff --git a/src/org/kne/cloud/network/klalb/ui/NodeInformationPanel.java b/src/org/kne/cloud/network/klalb/ui/NodeInformationPanel.java index 079315d..8ffa01a 100644 --- a/src/org/kne/cloud/network/klalb/ui/NodeInformationPanel.java +++ b/src/org/kne/cloud/network/klalb/ui/NodeInformationPanel.java @@ -4,9 +4,9 @@ import java.awt.BorderLayout; import java.awt.Image; import java.awt.Toolkit; import java.awt.datatransfer.StringSelection; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.List; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; import javax.swing.Icon; import javax.swing.ImageIcon; @@ -22,10 +22,13 @@ import javax.swing.event.ListSelectionListener; import org.kne.cloud.klalb.uitool.XDefaultListModel; import org.kne.cloud.network.MultiProtocolSocketAddress; -import org.kne.cloud.network.ipv6.IPv6Address; -import org.kne.cloud.network.klalb.KLALBController; -import org.kne.cloud.network.srv6.KLALBRoutingProtocol; -import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; +import org.kne.cloud.network.ipv6.IPv6Address; +import org.kne.cloud.network.klalb.KLALBController; +import org.kne.cloud.network.klalb.NodeInfoQueryCoordinator; +import org.kne.cloud.network.srv6.KLALBRoutingProtocol; +import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; +import org.kne.cloud.network.srv6.NodeInfoQueryStatus; +import org.kne.cloud.network.srv6.NodeProfile; import javax.swing.JTabbedPane; import javax.swing.JList; import javax.swing.JMenuItem; @@ -37,8 +40,9 @@ import java.awt.event.ActionEvent; import java.util.concurrent.atomic.AtomicLong; public class NodeInformationPanel extends JPanel { - private IPv6Address address; - private KLALBRoutingProtocolAPIClient client; + private IPv6Address address; + private KLALBRoutingProtocolAPIClient client; + private NodeInfoQueryCoordinator queryCoordinator; private KLALBController controller; private Image image; @@ -174,6 +178,7 @@ public class NodeInformationPanel extends JPanel { KLALBRoutingProtocol routingProtocol=controller.getIpv6Router().getKlalbRouteProtol(); client=new KLALBRoutingProtocolAPIClient(routingProtocol); + queryCoordinator=new NodeInfoQueryCoordinator(client); nodeInfoUpdateListener=(updatedAddress, flags) -> { if(active && address.equals(updatedAddress) && (flags & org.kne.cloud.network.srv6.RouterInfoPacket.NODE_INFO_FULL_UPDATE_REQUIRED) != 0) { @@ -186,27 +191,77 @@ public class NodeInformationPanel extends JPanel { private void requestFullInfo(JTextArea overviewArea, javax.swing.JLabel extraRoutesEmptyLabel) { final long generation=fullInfoGeneration.incrementAndGet(); + if (controller.getIpv6Router().getLocator().getAddress().equals(address)) { + KLALBController.PublishedNodeInfo published=controller.getPublishedNodeInfo(); + applyNodeInfo(overviewArea, extraRoutesEmptyLabel, published.getDeviceName(), + published.getDeviceDescription(), published.getExternalEndpoints(), published.getExtraRoutes()); + return; + } try { - client.requestNodeInfoFull(new InetSocketAddress( address.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), (info)->{ + queryCoordinator.query(new InetSocketAddress(address.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT)).whenComplete((info, error)->{ if(!active || generation!=fullInfoGeneration.get()) return; javax.swing.SwingUtilities.invokeLater(() -> { if(!active || generation!=fullInfoGeneration.get()) return; - listModel.clear(); - if(info.getOpenLines()!=null) for (MultiProtocolSocketAddress item : info.getOpenLines()) listModel.addElement(item); - extraRoutesModel.clear(); - List routes=info.getExtraRoutes(); - if(routes!=null) for(String route : routes) extraRoutesModel.addElement(route); - extraRoutesEmptyLabel.setText(UIEnv.getRsb().getString("noextraroutes")); - extraRoutesEmptyLabel.setVisible(extraRoutesModel.isEmpty()); - String dd=info.getDeviceDescription(); - overviewArea.setText(buildOverviewText(info.getDeviceName(), dd==null||dd.isEmpty()?null:dd)); + if (error != null || info == null) { + return; + } + applyRemoteNodeInfo(overviewArea, extraRoutesEmptyLabel, info); }); }); - } catch (IOException e) { + } catch (RuntimeException e) { if(active) e.printStackTrace(); } } + private void applyNodeInfo(JTextArea overviewArea, javax.swing.JLabel extraRoutesEmptyLabel, + String deviceName, String deviceDescription, List endpoints, + List routes) { + applyProfile(overviewArea, deviceName, deviceDescription); + applyEndpoints(endpoints == null ? Collections.emptyList() : endpoints); + applyRoutes(extraRoutesEmptyLabel, routes == null ? Collections.emptyList() : routes); + } + + private void applyRemoteNodeInfo(JTextArea overviewArea, javax.swing.JLabel extraRoutesEmptyLabel, + NodeInfoQueryCoordinator.Result info) { + if (info.getProfile().isReceived()) { + NodeProfile profile=info.getProfile().getValue().orElse(null); + applyProfile(overviewArea, profile == null ? null : profile.getDeviceName(), + profile == null ? null : profile.getDeviceDescription()); + } + + if (info.getExternalEndpoints().isReceived()) { + List endpoints=info.getExternalEndpoints().getStatus() + .filter(NodeInfoQueryStatus.OK::equals).isPresent() + ? info.getExternalEndpoints().getValue().orElse(Collections.emptyList()) + : Collections.emptyList(); + applyEndpoints(endpoints); + } + + if (info.getExtraRoutes().isReceived()) { + List routes=info.getExtraRoutes().getStatus().filter(NodeInfoQueryStatus.OK::equals).isPresent() + ? info.getExtraRoutes().getValue().orElse(Collections.emptyList()) + : Collections.emptyList(); + applyRoutes(extraRoutesEmptyLabel, routes); + } +} + + private void applyProfile(JTextArea overviewArea, String deviceName, String deviceDescription) { + overviewArea.setText(buildOverviewText(deviceName, + deviceDescription == null || deviceDescription.isEmpty() ? null : deviceDescription)); +} + + private void applyEndpoints(List endpoints) { + listModel.clear(); + if (endpoints != null) for (MultiProtocolSocketAddress item : endpoints) listModel.addElement(item); + } + + private void applyRoutes(javax.swing.JLabel extraRoutesEmptyLabel, List routes) { + extraRoutesModel.clear(); + if (routes != null) for (String route : routes) extraRoutesModel.addElement(route); + extraRoutesEmptyLabel.setText(UIEnv.getRsb().getString("noextraroutes")); + extraRoutesEmptyLabel.setVisible(extraRoutesModel.isEmpty()); + } + public synchronized void close() { if (!active) return; diff --git a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java index 430f57d..63a411f 100644 --- a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java +++ b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java @@ -20,10 +20,11 @@ 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.KLALBNodeInformation; import org.kne.cloud.network.srv6.KLALBRoutingProtocol; import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection; import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient; +import org.kne.cloud.network.srv6.NodeInfoQueryStatus; +import org.kne.cloud.network.srv6.NodeProfile; import org.kne.cloud.network.srv6.NeighborInfo; import org.kne.cloud.network.srv6.RouterInfo; import org.kne.cloud.network.srv6.RouterInfoPacket; @@ -54,7 +55,7 @@ public class KLALBWebServer { this.gson = proxySystem.getGson(); this.nodeInfoUpdateListener = (address, flags) -> { if ((flags & RouterInfoPacket.NODE_INFO_FULL_UPDATE_REQUIRED) != 0) { - nodeInfoFullRevisions.merge(address, 1L, Long::sum); + nodeInfoRevisions.merge(address, 1L, Long::sum); } if ((flags & RouterInfoPacket.NODE_INFO_TINY_UPDATE_REQUIRED) != 0) { long lifecycleGeneration; @@ -73,7 +74,6 @@ public class KLALBWebServer { public synchronized void start() throws IOException { if (running.get()) return; - nodeInfoFullRevisionEpoch = UUID.randomUUID().toString(); if (sseExecutor == null || sseExecutor.isShutdown()) { sseExecutor = createSseExecutor(); } @@ -503,14 +503,13 @@ public class KLALBWebServer { private final Set tinyNameRequestsInFlight = ConcurrentHashMap.newKeySet(); private final ConcurrentMap tinyNameRequestTimes = new ConcurrentHashMap<>(); private final ConcurrentMap tinyNameRequestGenerations = new ConcurrentHashMap<>(); - private final ConcurrentMap nodeInfoFullRevisions = new ConcurrentHashMap<>(); + private final ConcurrentMap nodeInfoRevisions = new ConcurrentHashMap<>(); private final Object tinyNameStateLock = new Object(); private static final long TINY_NAME_CACHE_TTL_MS = 60000L; private long nextTinyNameRequestGeneration; private KLALBRoutingProtocol nodeInfoRoutingProtocol; private final BiConsumer nodeInfoUpdateListener; private long nodeInfoLifecycleGeneration; - private volatile String nodeInfoFullRevisionEpoch = UUID.randomUUID().toString(); private static ScheduledExecutorService createSseExecutor() { return Executors.newSingleThreadScheduledExecutor(r -> { @@ -605,14 +604,14 @@ public class KLALBWebServer { } return ""; } - client.requestNodeInfoTiny( + client.requestNodeProfile( new InetSocketAddress(addr.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), - info -> { + profile -> { synchronized (tinyNameStateLock) { Long activeRequestGeneration = tinyNameRequestGenerations.get(addr); if (!Long.valueOf(requestGenerationToken).equals(activeRequestGeneration)) return; tinyDeviceNameCache.put(addr, - info != null && info.getDeviceName() != null ? info.getDeviceName() : ""); + profile != null && profile.getDeviceName() != null ? profile.getDeviceName() : ""); tinyDeviceNameCacheTimes.put(addr, System.currentTimeMillis()); tinyNameRequestsInFlight.remove(addr); tinyNameRequestTimes.remove(addr); @@ -689,6 +688,7 @@ public class KLALBWebServer { // 本机:描述直接取本地控制器配置 KLALBController.PublishedNodeInfo published = kc.getPublishedNodeInfo(); resp.addProperty("isSelf", true); + resp.addProperty("reachable", true); resp.addProperty("deviceName", published.getDeviceName() != null ? published.getDeviceName() : ""); resp.addProperty("deviceDescription", published.getDeviceDescription() != null ? published.getDeviceDescription() : ""); @@ -710,41 +710,36 @@ public class KLALBWebServer { // 远端节点:经 SRv6 虚拟网络发送完整节点信息查询(异步回调,限时等待) resp.addProperty("isSelf", false); - CompletableFuture future = new CompletableFuture<>(); try { KLALBRoutingProtocolAPIClient client = getNodeInfoClient(kc); if (client == null) { sendError(exchange, 503, "Node info service stopped"); return; } - client.requestNodeInfoFull( - new InetSocketAddress(target.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), - info -> future.complete(info)); - - KLALBNodeInformation info; - try { - info = future.get(3, TimeUnit.SECONDS); - } catch (TimeoutException te) { - info = null; - } - - String dname = info != null && info.getDeviceName() != null && !info.getDeviceName().isEmpty() - ? info.getDeviceName() - : ""; + NodeInfoQueryCoordinator.Result info = new NodeInfoQueryCoordinator(client) + .query(new InetSocketAddress(target.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT)).get(); + NodeInfoQueryCoordinator.Section profile = info.getProfile(); + NodeProfile nodeProfile = profile.getValue().orElse(null); + String dname = nodeProfile != null && nodeProfile.getDeviceName() != null + && !nodeProfile.getDeviceName().isEmpty() ? nodeProfile.getDeviceName() : ""; resp.addProperty("deviceName", dname); resp.addProperty("deviceDescription", - info != null && info.getDeviceDescription() != null ? info.getDeviceDescription() : ""); - resp.addProperty("reachable", info != null); + nodeProfile != null && nodeProfile.getDeviceDescription() != null + ? nodeProfile.getDeviceDescription() : ""); + resp.addProperty("reachable", profile.isReceived() + || info.getExternalEndpoints().isReceived() || info.getExtraRoutes().isReceived()); JsonArray lines = new JsonArray(); - if (info != null && info.getOpenLines() != null) { - for (MultiProtocolSocketAddress mpsa : info.getOpenLines()) { + if (info.getExternalEndpoints().getStatus().filter(NodeInfoQueryStatus.OK::equals).isPresent() + && info.getExternalEndpoints().getValue().isPresent()) { + for (MultiProtocolSocketAddress mpsa : info.getExternalEndpoints().getValue().get()) { lines.add(new JsonPrimitive(mpsa.toString())); } } resp.add("openLines", lines); JsonArray extraRoutes = new JsonArray(); - if (info != null && info.getExtraRoutes() != null) { - for (String route : info.getExtraRoutes()) { + if (info.getExtraRoutes().getStatus().filter(NodeInfoQueryStatus.OK::equals).isPresent() + && info.getExtraRoutes().getValue().isPresent()) { + for (String route : info.getExtraRoutes().getValue().get()) { extraRoutes.add(new JsonPrimitive(route != null ? route : "")); } } @@ -799,7 +794,7 @@ public class KLALBWebServer { tinyNameRequestTimes.keySet().removeIf(address -> !activeAddresses.contains(address)); tinyNameRequestGenerations.keySet().removeIf(address -> !activeAddresses.contains(address)); } - nodeInfoFullRevisions.keySet().removeIf(address -> !activeAddresses.contains(address)); + nodeInfoRevisions.keySet().removeIf(address -> !activeAddresses.contains(address)); if (addrs != null) { for (IPv6Address addr : addrs.keySet()) { @@ -811,10 +806,9 @@ public class KLALBWebServer { String dname = addr.equals(selfAddr) ? kc.getIpv6Router().getDeviceName() : getTinyDeviceName(kc, addr); nodeObj.addProperty("deviceName", dname != null ? dname : ""); - long fullRevision = addr.equals(selfAddr) ? published.getFullRevision() - : nodeInfoFullRevisions.getOrDefault(addr, 0L); - nodeObj.addProperty("nodeInfoFullRevision", fullRevision); - nodeObj.addProperty("nodeInfoFullRevisionEpoch", nodeInfoFullRevisionEpoch); + long nodeInfoRevision = addr.equals(selfAddr) ? published.getFullRevision() + : nodeInfoRevisions.getOrDefault(addr, 0L); + nodeObj.addProperty("nodeInfoRevision", nodeInfoRevision); nodesArray.add(nodeObj); } } diff --git a/src/org/kne/cloud/network/srv6/ExternalEndpointsResult.java b/src/org/kne/cloud/network/srv6/ExternalEndpointsResult.java new file mode 100644 index 0000000..6d7205d --- /dev/null +++ b/src/org/kne/cloud/network/srv6/ExternalEndpointsResult.java @@ -0,0 +1,49 @@ +package org.kne.cloud.network.srv6; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.kne.cloud.network.MultiProtocolSocketAddress; + +public final class ExternalEndpointsResult { + private final List externalEndpoints; + private final NodeInfoQueryStatus status; + + public ExternalEndpointsResult(List externalEndpoints, NodeInfoQueryStatus status) { + this.externalEndpoints = Collections.unmodifiableList(new ArrayList( + externalEndpoints == null ? Collections.emptyList() : externalEndpoints)); + this.status = status; + } + + public List getExternalEndpoints() { + return externalEndpoints; + } + + public NodeInfoQueryStatus getStatus() { + return status; + } + + @Override + public int hashCode() { + return Objects.hash(externalEndpoints, status); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ExternalEndpointsResult)) { + return false; + } + ExternalEndpointsResult other = (ExternalEndpointsResult) obj; + return Objects.equals(externalEndpoints, other.externalEndpoints) && status == other.status; + } + + @Override + public String toString() { + return "ExternalEndpointsResult [externalEndpoints=" + externalEndpoints + ", status=" + status + "]"; + } +} diff --git a/src/org/kne/cloud/network/srv6/ExtraRoutesResult.java b/src/org/kne/cloud/network/srv6/ExtraRoutesResult.java new file mode 100644 index 0000000..50ba854 --- /dev/null +++ b/src/org/kne/cloud/network/srv6/ExtraRoutesResult.java @@ -0,0 +1,47 @@ +package org.kne.cloud.network.srv6; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public final class ExtraRoutesResult { + private final List extraRoutes; + private final NodeInfoQueryStatus status; + + public ExtraRoutesResult(List extraRoutes, NodeInfoQueryStatus status) { + this.extraRoutes = Collections.unmodifiableList(new ArrayList( + extraRoutes == null ? Collections.emptyList() : extraRoutes)); + this.status = status; + } + + public List getExtraRoutes() { + return extraRoutes; + } + + public NodeInfoQueryStatus getStatus() { + return status; + } + + @Override + public int hashCode() { + return Objects.hash(extraRoutes, status); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ExtraRoutesResult)) { + return false; + } + ExtraRoutesResult other = (ExtraRoutesResult) obj; + return Objects.equals(extraRoutes, other.extraRoutes) && status == other.status; + } + + @Override + public String toString() { + return "ExtraRoutesResult [extraRoutes=" + extraRoutes + ", status=" + status + "]"; + } +} diff --git a/src/org/kne/cloud/network/srv6/KLALBNodeInformation.java b/src/org/kne/cloud/network/srv6/KLALBNodeInformation.java deleted file mode 100644 index 6a94ba6..0000000 --- a/src/org/kne/cloud/network/srv6/KLALBNodeInformation.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.kne.cloud.network.srv6; - -import java.util.List; -import java.util.ArrayList; - -import org.kne.cloud.network.MultiProtocolSocketAddress; - -/** - * 节点信息(开放线路 + 设备名称 + 设备描述),由路由协议 JSON API 查询获得。 - */ -public class KLALBNodeInformation { - private List openLines; - private String deviceName; - private String deviceDescription; - private List extraRoutes; - - public KLALBNodeInformation(List openLines, String deviceName, - String deviceDescription) { - this(openLines, deviceName, deviceDescription, new ArrayList()); - } - - public KLALBNodeInformation(List openLines, String deviceName, - String deviceDescription, List extraRoutes) { - super(); - this.openLines = openLines; - this.deviceName = deviceName; - this.deviceDescription = deviceDescription; - this.extraRoutes = extraRoutes == null ? new ArrayList() : extraRoutes; - } - - public List getOpenLines() { - return openLines; - } - - public void setOpenLines(List openLines) { - this.openLines = openLines; - } - - public String getDeviceName() { - return deviceName; - } - - public void setDeviceName(String deviceName) { - this.deviceName = deviceName; - } - - public String getDeviceDescription() { - return deviceDescription; - } - - public void setDeviceDescription(String deviceDescription) { - this.deviceDescription = deviceDescription; - } - - public List getExtraRoutes() { - return extraRoutes; - } - - public void setExtraRoutes(List extraRoutes) { - this.extraRoutes = extraRoutes == null ? new ArrayList() : extraRoutes; - } - - @Override - public String toString() { - return "KLALBNodeInformation [openLines=" + openLines + ", deviceName=" + deviceName - + ", deviceDescription=" + deviceDescription + ", extraRoutes=" + extraRoutes + "]"; - } -} diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java index f0915da..07671df 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java @@ -1,129 +1,208 @@ -package org.kne.cloud.network.srv6; - -import java.io.IOException; -import java.lang.ref.Cleaner; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import org.kne.cloud.network.MultiProtocolSocketAddress; -import org.kne.cloud.network.congestion.NOCongestionAlgorithm; -import org.kne.cloud.network.congestion.SendPacketSlidingWindow; -import org.kne.opencl64.Releaser; - -public class KLALBRoutingProtocolAPIClient { - private KLALBRoutingProtocol routingProtocol; - private SendPacketSlidingWindow window = new SendPacketSlidingWindow( - new NOCongestionAlgorithm(3000000000L), 1024 * 1024); - - private static final Cleaner clr = Cleaner.create(); - - private BiConsumer rec = (addr, data) -> { - KLALBRoutingProtocolJsonData dataobj = data.getDecodedData(); - InetSocketAddress addrs = (InetSocketAddress) addr; - UUID ruid = dataobj.getUuid(); - JsonDataPacket relate = null; - //System.out.println(ruid + " " + window.getSendmap()); - switch (dataobj.getType()) { - case KLALBRoutingProtocolJsonData.NODE_INFO_FULL_RESP: - case KLALBRoutingProtocolJsonData.NODE_INFO_TINY_RESP: - - if ((relate = window.ack(ruid)) != null) { - List connects = (List) dataobj.getData(); - List connectsm = new ArrayList( - connects == null ? 0 : connects.size()); - if (connects != null) { - for (Object open : connects) { - if (open instanceof MultiProtocolSocketAddress) { - connectsm.add((MultiProtocolSocketAddress) open); - } else { - connectsm.add(new MultiProtocolSocketAddress((String) open)); - - } - } +package org.kne.cloud.network.srv6; + +import java.io.IOException; +import java.lang.ref.Cleaner; +import java.net.InetSocketAddress; +import java.net.InetAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.kne.cloud.network.MultiProtocolSocketAddress; +import org.kne.cloud.network.congestion.NOCongestionAlgorithm; +import org.kne.cloud.network.congestion.SendPacketSlidingWindow; +import org.kne.cloud.network.klalb.SendItem; +import org.kne.opencl64.Releaser; + +public class KLALBRoutingProtocolAPIClient { + private KLALBRoutingProtocol routingProtocol; + private SendPacketSlidingWindow window = new SendPacketSlidingWindow( + new NOCongestionAlgorithm(3000000000L), 1024 * 1024); + + private static final Cleaner clr = Cleaner.create(); + + private BiConsumer rec = (addr, data) -> { + KLALBRoutingProtocolJsonData dataobj = data.getDecodedData(); + UUID ruid = dataobj.getUuid(); + switch (dataobj.getType()) { + case KLALBRoutingProtocolJsonData.NODE_PROFILE_RESP: + PendingRequest profileRequest = acknowledgeResponse(ruid, + KLALBRoutingProtocolJsonData.NODE_PROFILE_REQ, addr); + if (profileRequest != null) { + ((Consumer) profileRequest.callback) + .accept(new NodeProfile(dataobj.getDeviceName(), dataobj.getDeviceDescription())); + } + break; + case KLALBRoutingProtocolJsonData.NODE_EXTERNAL_ENDPOINTS_RESP: + PendingRequest endpointsRequest = acknowledgeResponse(ruid, + KLALBRoutingProtocolJsonData.NODE_EXTERNAL_ENDPOINTS_REQ, addr); + if (endpointsRequest != null) { + ((Consumer) endpointsRequest.callback).accept( + new ExternalEndpointsResult(decodeExternalEndpoints(dataobj.getData()), dataobj.getStatus())); + } + break; + case KLALBRoutingProtocolJsonData.NODE_EXTRA_ROUTES_RESP: + PendingRequest routesRequest = acknowledgeResponse(ruid, + KLALBRoutingProtocolJsonData.NODE_EXTRA_ROUTES_REQ, addr); + if (routesRequest != null) { + ((Consumer) routesRequest.callback).accept( + new ExtraRoutesResult(decodeExtraRoutes(dataobj.getData()), dataobj.getStatus())); + } + break; + } + }; + + private PendingRequest acknowledgeResponse(UUID uuid, String expectedRequestType, SocketAddress sourceAddress) { + SendItem pending = window.getSendmap().get(uuid); + if (pending == null) { + return null; + } + KLALBRoutingProtocolJsonData request = pending.getPacket().getDecodedData(); + if (request == null || !expectedRequestType.equals(request.getType())) { + return null; + } + Object callback = pending.getPacket().getUserCallback(); + if (!(callback instanceof PendingRequest)) { + return null; + } + PendingRequest pendingRequest = (PendingRequest) callback; + if (!expectedRequestType.equals(pendingRequest.expectedRequestType) + || !sameDestination(pendingRequest.expectedDestination, sourceAddress)) { + return null; + } + return window.ack(uuid) == null ? null : pendingRequest; + } + + private static boolean sameDestination(SocketAddress expected, SocketAddress actual) { + if (expected instanceof InetSocketAddress && actual instanceof InetSocketAddress) { + InetSocketAddress expectedInet = (InetSocketAddress) expected; + InetSocketAddress actualInet = (InetSocketAddress) actual; + if (expectedInet.getPort() != actualInet.getPort()) return false; + InetAddress expectedAddress = expectedInet.getAddress(); + InetAddress actualAddress = actualInet.getAddress(); + if (expectedAddress != null && actualAddress != null) return expectedAddress.equals(actualAddress); + if (expectedAddress == null && actualAddress == null) { + return expectedInet.getHostString().equalsIgnoreCase(actualInet.getHostString()); + } + return false; + } + return expected != null && expected.equals(actual); + } + + private List decodeExternalEndpoints(Object data) { + if (!(data instanceof List)) { + return new ArrayList(); + } + List encoded = (List) data; + List endpoints = new ArrayList(encoded.size()); + for (Object value : encoded) { + if (value instanceof MultiProtocolSocketAddress) { + endpoints.add((MultiProtocolSocketAddress) value); + } else if (value instanceof String) { + try { + endpoints.add(new MultiProtocolSocketAddress((String) value)); + } catch (RuntimeException ignored) { + // Ignore malformed entries while preserving the rest of the response. } - List extraRoutes = dataobj.getExtraRoutes(); - if(extraRoutes == null) { - extraRoutes = new ArrayList(); - } - // 组装节点信息(线路 + 设备名称 + 设备描述,精简模式下线路与描述为 null) - KLALBNodeInformation info = new KLALBNodeInformation(connectsm, dataobj.getDeviceName(), - dataobj.getDeviceDescription(), extraRoutes); - ((Consumer) relate.getUserCallback()).accept(info); - } - break; - } - - }; - - public KLALBRoutingProtocolAPIClient(KLALBRoutingProtocol routingProtocol) { - this.routingProtocol = routingProtocol; - routingProtocol.addReceiver(rec); - this.releaser = new KLALBRoutingProtocolAPIClientReleaser(this.routingProtocol, rec, window); - clr.register(this, releaser); - } - - /** - * 精简查询:仅获取对端设备名称(开销最小,适用于未查看节点详情的场景)。 - */ - public void requestNodeInfoTiny(SocketAddress addr, Consumer callback) - throws IOException { - sendNodeInfoRequest(KLALBRoutingProtocolJsonData.NODE_INFO_TINY_REQ, addr, callback); - } - - /** - * 完整查询:获取对端开放线路 + 设备名称 + 设备描述(查看节点信息时使用)。 - */ - public void requestNodeInfoFull(SocketAddress addr, Consumer callback) - throws IOException { - sendNodeInfoRequest(KLALBRoutingProtocolJsonData.NODE_INFO_FULL_REQ, addr, callback); - } - - private void sendNodeInfoRequest(String type, SocketAddress addr, Consumer callback) - throws IOException { - UUID suid = UUID.randomUUID(); - KLALBRoutingProtocolJsonData json = new KLALBRoutingProtocolJsonData(type, suid, null); - JsonDataPacket packet = new JsonDataPacket(json); - packet.setUserCallback(callback); - window.put(suid, packet); - routingProtocol.sendJsonPacketToAddress(packet, addr); - - } - - public KLALBRoutingProtocol getRoutingProtocol() { - return routingProtocol; - } - - private KLALBRoutingProtocolAPIClientReleaser releaser; - - public void close() { - releaser.run(); - } - - public boolean isClosed() { - return releaser.isReleased(); - } -} - -class KLALBRoutingProtocolAPIClientReleaser extends Releaser> { - - private KLALBRoutingProtocol routingProtocol; - private SendPacketSlidingWindow window; - - public KLALBRoutingProtocolAPIClientReleaser(KLALBRoutingProtocol routingProtocol, - BiConsumer resource, SendPacketSlidingWindow window) { - super(resource); - this.routingProtocol = routingProtocol; - this.window = window; - } - - @Override - protected void release(BiConsumer resource) { - window.close(); - routingProtocol.removeReceiver(resource); - } + } + } + return endpoints; + } + + private List decodeExtraRoutes(Object data) { + if (!(data instanceof List)) { + return new ArrayList(); + } + List encoded = (List) data; + List routes = new ArrayList(encoded.size()); + for (Object value : encoded) { + if (value instanceof String) { + routes.add((String) value); + } + } + return routes; + } + + public KLALBRoutingProtocolAPIClient(KLALBRoutingProtocol routingProtocol) { + this.routingProtocol = routingProtocol; + routingProtocol.addReceiver(rec); + this.releaser = new KLALBRoutingProtocolAPIClientReleaser(this.routingProtocol, rec, window); + clr.register(this, releaser); + } + + public void requestNodeProfile(SocketAddress addr, Consumer callback) throws IOException { + sendNodeInfoRequest(KLALBRoutingProtocolJsonData.NODE_PROFILE_REQ, addr, callback); + } + + public void requestExternalEndpoints(SocketAddress addr, Consumer callback) + throws IOException { + sendNodeInfoRequest(KLALBRoutingProtocolJsonData.NODE_EXTERNAL_ENDPOINTS_REQ, addr, callback); + } + + public void requestExtraRoutes(SocketAddress addr, Consumer callback) throws IOException { + sendNodeInfoRequest(KLALBRoutingProtocolJsonData.NODE_EXTRA_ROUTES_REQ, addr, callback); + } + + private void sendNodeInfoRequest(String type, SocketAddress addr, Object callback) throws IOException { + UUID suid = UUID.randomUUID(); + KLALBRoutingProtocolJsonData json = new KLALBRoutingProtocolJsonData(type, suid, null); + JsonDataPacket packet = new JsonDataPacket(json); + packet.setUserCallback(new PendingRequest(addr, type, callback)); + synchronized (this) { + if (closed.get() || releaser.isReleased()) throw new IOException("Node info client is closed"); + window.put(suid, packet); + routingProtocol.sendJsonPacketToAddress(packet, addr); + } + } + + public KLALBRoutingProtocol getRoutingProtocol() { + return routingProtocol; + } + + private KLALBRoutingProtocolAPIClientReleaser releaser; + + public synchronized void close() { + if (closed.compareAndSet(false, true)) releaser.run(); + } + + public boolean isClosed() { + return closed.get() || releaser.isReleased(); + } + + private static final class PendingRequest { + private final SocketAddress expectedDestination; + private final String expectedRequestType; + private final Object callback; + + private PendingRequest(SocketAddress expectedDestination, String expectedRequestType, Object callback) { + this.expectedDestination = expectedDestination; + this.expectedRequestType = expectedRequestType; + this.callback = callback; + } + } + + private final AtomicBoolean closed = new AtomicBoolean(); +} + +class KLALBRoutingProtocolAPIClientReleaser extends Releaser> { + + private KLALBRoutingProtocol routingProtocol; + private SendPacketSlidingWindow window; + + public KLALBRoutingProtocolAPIClientReleaser(KLALBRoutingProtocol routingProtocol, + BiConsumer resource, SendPacketSlidingWindow window) { + super(resource); + this.routingProtocol = routingProtocol; + this.window = window; + } + + @Override + protected void release(BiConsumer resource) { + window.close(); + routingProtocol.removeReceiver(resource); + } } diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java index d6e81f0..01a3cc5 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java @@ -1,91 +1,98 @@ -package org.kne.cloud.network.srv6; - -import java.io.IOException; -import java.lang.ref.Cleaner; -import java.net.InetSocketAddress; +package org.kne.cloud.network.srv6; + +import java.io.IOException; +import java.lang.ref.Cleaner; import java.net.SocketAddress; import java.util.ArrayList; -import java.util.List; import java.util.function.BiConsumer; - -import org.kne.cloud.network.klalb.KLALBController; -import org.kne.opencl64.Releaser; - -public class KLALBRoutingProtocolAPIServer { - private KLALBController controller; - private KLALBRoutingProtocol routingProtocol; - - - private static final Cleaner clr=Cleaner.create(); - - private BiConsumer rec=(addr,data)->{ - try { - KLALBRoutingProtocolJsonData dataobj= data.getDecodedData(); - InetSocketAddress addrs=(InetSocketAddress) addr; - switch(dataobj.getType()){ - case KLALBRoutingProtocolJsonData.NODE_INFO_TINY_REQ: - // 精简查询:仅返回设备名称(设备名称随路由信息广播公开,不受 denyExternalEndpointQuery 限制) - KLALBController.PublishedNodeInfo published = controller.getPublishedNodeInfo(); - KLALBRoutingProtocolJsonData tinyjson=new KLALBRoutingProtocolJsonData(KLALBRoutingProtocolJsonData.NODE_INFO_TINY_RESP,dataobj.getUuid(),null); - tinyjson.setDeviceName(published.getDeviceName()); - routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(tinyjson),addr); - break; - case KLALBRoutingProtocolJsonData.NODE_INFO_FULL_REQ: - // 完整查询:设备名称与描述总是正常响应;denyExternalEndpointQuery 仅隐藏外部端点列表 - boolean denyEndpoints=controller.getConfigItem()!=null&&controller.getConfigItem().isDenyExternalEndpointQuery(); - KLALBController.PublishedNodeInfo fullPublished = controller.getPublishedNodeInfo(); - KLALBRoutingProtocolJsonData json=new KLALBRoutingProtocolJsonData(KLALBRoutingProtocolJsonData.NODE_INFO_FULL_RESP,dataobj.getUuid(),denyEndpoints?null:new ArrayList(fullPublished.getExternalEndpoints())); - json.setDeviceName(fullPublished.getDeviceName());// 附带本机设备名称 - json.setDeviceDescription(fullPublished.getDeviceDescription());// 附带本机设备描述 - json.setExtraRoutes(new ArrayList(fullPublished.getExtraRoutes())); - routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(json),addr); - break; - } - } catch (IOException e) { - e.printStackTrace(); - } - }; - - public KLALBRoutingProtocolAPIServer(KLALBRoutingProtocol routingProtocol,KLALBController controller) { - this.controller=controller; - this.routingProtocol=routingProtocol; - routingProtocol.addReceiver(rec); - this.releaser=new KLALBRoutingProtocolAPIServerReleaser(this.routingProtocol,rec); - clr.register(this, releaser); - } - - public KLALBController getController() { - return controller; - } - - public KLALBRoutingProtocol getRoutingProtocol() { - return routingProtocol; - } - - -private KLALBRoutingProtocolAPIServerReleaser releaser; - - public void close() { - releaser.run(); - } - - - public boolean isClosed() { - return releaser.isReleased(); - } - -} -class KLALBRoutingProtocolAPIServerReleaser extends Releaser>{ - - private KLALBRoutingProtocol routingProtocol; - - public KLALBRoutingProtocolAPIServerReleaser(KLALBRoutingProtocol routingProtocol,BiConsumer resource) { - super(resource); - this.routingProtocol=routingProtocol; - } - - @Override - protected void release(BiConsumer resource) { - routingProtocol.removeReceiver(resource); - } + +import org.kne.cloud.network.klalb.KLALBController; +import org.kne.opencl64.Releaser; + +public class KLALBRoutingProtocolAPIServer { + private KLALBController controller; + private KLALBRoutingProtocol routingProtocol; + + private static final Cleaner clr = Cleaner.create(); + + private BiConsumer rec = (addr, data) -> { + try { + KLALBRoutingProtocolJsonData dataobj = data.getDecodedData(); + switch (dataobj.getType()) { + case KLALBRoutingProtocolJsonData.NODE_PROFILE_REQ: + KLALBController.PublishedNodeInfo profilePublished = controller.getPublishedNodeInfo(); + KLALBRoutingProtocolJsonData profile = new KLALBRoutingProtocolJsonData( + KLALBRoutingProtocolJsonData.NODE_PROFILE_RESP, dataobj.getUuid(), null); + profile.setDeviceName(profilePublished.getDeviceName()); + profile.setDeviceDescription(profilePublished.getDeviceDescription()); + profile.setStatus(NodeInfoQueryStatus.OK); + routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(profile), addr); + break; + case KLALBRoutingProtocolJsonData.NODE_EXTERNAL_ENDPOINTS_REQ: + boolean denyEndpoints = controller.getConfigItem() != null + && controller.getConfigItem().isDenyExternalEndpointQuery(); + KLALBController.PublishedNodeInfo endpointsPublished = controller.getPublishedNodeInfo(); + KLALBRoutingProtocolJsonData endpoints = new KLALBRoutingProtocolJsonData( + KLALBRoutingProtocolJsonData.NODE_EXTERNAL_ENDPOINTS_RESP, dataobj.getUuid(), + denyEndpoints ? new ArrayList() + : new ArrayList( + endpointsPublished.getExternalEndpoints())); + endpoints.setStatus(denyEndpoints ? NodeInfoQueryStatus.DENIED : NodeInfoQueryStatus.OK); + routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(endpoints), addr); + break; + case KLALBRoutingProtocolJsonData.NODE_EXTRA_ROUTES_REQ: + KLALBController.PublishedNodeInfo routesPublished = controller.getPublishedNodeInfo(); + KLALBRoutingProtocolJsonData routes = new KLALBRoutingProtocolJsonData( + KLALBRoutingProtocolJsonData.NODE_EXTRA_ROUTES_RESP, dataobj.getUuid(), + new ArrayList(routesPublished.getExtraRoutes())); + routes.setStatus(NodeInfoQueryStatus.OK); + routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(routes), addr); + break; + } + } catch (IOException e) { + e.printStackTrace(); + } + }; + + public KLALBRoutingProtocolAPIServer(KLALBRoutingProtocol routingProtocol, KLALBController controller) { + this.controller = controller; + this.routingProtocol = routingProtocol; + routingProtocol.addReceiver(rec); + this.releaser = new KLALBRoutingProtocolAPIServerReleaser(this.routingProtocol, rec); + clr.register(this, releaser); + } + + public KLALBController getController() { + return controller; + } + + public KLALBRoutingProtocol getRoutingProtocol() { + return routingProtocol; + } + + private KLALBRoutingProtocolAPIServerReleaser releaser; + + public void close() { + releaser.run(); + } + + public boolean isClosed() { + return releaser.isReleased(); + } +} + +class KLALBRoutingProtocolAPIServerReleaser extends Releaser> { + + private KLALBRoutingProtocol routingProtocol; + + public KLALBRoutingProtocolAPIServerReleaser(KLALBRoutingProtocol routingProtocol, + BiConsumer resource) { + super(resource); + this.routingProtocol = routingProtocol; + } + + @Override + protected void release(BiConsumer resource) { + routingProtocol.removeReceiver(resource); + } } diff --git a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java index 0e8e80c..b714a8d 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java @@ -1,109 +1,89 @@ -package org.kne.cloud.network.srv6; - +package org.kne.cloud.network.srv6; + +import java.util.Objects; import java.util.UUID; -import java.util.List; - -public class KLALBRoutingProtocolJsonData { - public static final String NODE_INFO_TINY_REQ="nodeinfotinyreq";// 精简查询:仅设备名称 - public static final String NODE_INFO_TINY_RESP="nodeinfotinyresp"; - public static final String NODE_INFO_FULL_REQ="nodeinfofullreq";// 完整查询:开放线路+设备名称+设备描述 - public static final String NODE_INFO_FULL_RESP="nodeinfofullresp"; - private String type; - private UUID uuid; - private Object data; - private String deviceName;// 对端设备名称 - private String deviceDescription;// 对端设备描述 - private List extraRoutes; - public String getDeviceName() { - return deviceName; - } - public void setDeviceName(String deviceName) { - this.deviceName = deviceName; - } - public String getDeviceDescription() { - return deviceDescription; - } + +public class KLALBRoutingProtocolJsonData { + public static final String NODE_PROFILE_REQ = "nodeprofilereq"; + public static final String NODE_PROFILE_RESP = "nodeprofileresp"; + public static final String NODE_EXTERNAL_ENDPOINTS_REQ = "nodeexternalendpointsreq"; + public static final String NODE_EXTERNAL_ENDPOINTS_RESP = "nodeexternalendpointsresp"; + public static final String NODE_EXTRA_ROUTES_REQ = "nodeextraroutesreq"; + public static final String NODE_EXTRA_ROUTES_RESP = "nodeextraroutesresp"; + + private String type; + private UUID uuid; + private Object data; + private String deviceName; + private String deviceDescription; + private NodeInfoQueryStatus status; + + public KLALBRoutingProtocolJsonData(String type, UUID uuid, Object data) { + this.type = type; + this.uuid = uuid; + this.data = data; + } + + public String getType() { + return type; + } + + public UUID getUuid() { + return uuid; + } + + public Object getData() { + return data; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public String getDeviceDescription() { + return deviceDescription; + } + public void setDeviceDescription(String deviceDescription) { this.deviceDescription = deviceDescription; } - public List getExtraRoutes() { - return extraRoutes; + + public NodeInfoQueryStatus getStatus() { + return status; } - public void setExtraRoutes(List extraRoutes) { - this.extraRoutes = extraRoutes; + + public void setStatus(NodeInfoQueryStatus status) { + this.status = status; } - public String getType() { - return type; - } - public Object getData() { - return data; - } - public UUID getUuid() { - return uuid; - } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((data == null) ? 0 : data.hashCode()); - result = prime * result + ((type == null) ? 0 : type.hashCode()); - result = prime * result + ((uuid == null) ? 0 : uuid.hashCode()); - result = prime * result + ((deviceName == null) ? 0 : deviceName.hashCode()); - result = prime * result + ((deviceDescription == null) ? 0 : deviceDescription.hashCode()); - result = prime * result + ((extraRoutes == null) ? 0 : extraRoutes.hashCode()); - return result; - } - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - KLALBRoutingProtocolJsonData other = (KLALBRoutingProtocolJsonData) obj; - if (data == null) { - if (other.data != null) - return false; - } else if (!data.equals(other.data)) - return false; - if (type == null) { - if (other.type != null) - return false; - } else if (!type.equals(other.type)) - return false; - if (uuid == null) { - if (other.uuid != null) - return false; - } else if (!uuid.equals(other.uuid)) - return false; - if (deviceName == null) { - if (other.deviceName != null) - return false; - } else if (!deviceName.equals(other.deviceName)) - return false; - if (deviceDescription == null) { - if (other.deviceDescription != null) - return false; - } else if (!deviceDescription.equals(other.deviceDescription)) + + @Override + public int hashCode() { + return Objects.hash(type, uuid, data, deviceName, deviceDescription, status); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof KLALBRoutingProtocolJsonData)) { return false; - if (extraRoutes == null) { - if (other.extraRoutes != null) - return false; - } else if (!extraRoutes.equals(other.extraRoutes)) - return false; - return true; - } - public KLALBRoutingProtocolJsonData(String type, UUID uuid, Object data) { - super(); - this.type = type; - this.uuid = uuid; - this.data = data; - } - @Override - public String toString() { - return "KLALBRoutingProtocolJsonData [type=" + type + ", uuid=" + uuid + ", data=" + data + ", deviceName=" - + deviceName + ", deviceDescription=" + deviceDescription + ", extraRoutes=" + extraRoutes + "]"; - } - -} + } + KLALBRoutingProtocolJsonData other = (KLALBRoutingProtocolJsonData) obj; + return Objects.equals(type, other.type) && Objects.equals(uuid, other.uuid) + && Objects.equals(data, other.data) && Objects.equals(deviceName, other.deviceName) + && Objects.equals(deviceDescription, other.deviceDescription) + && Objects.equals(status, other.status); + } + + @Override + public String toString() { + return "KLALBRoutingProtocolJsonData [type=" + type + ", uuid=" + uuid + ", data=" + data + + ", deviceName=" + deviceName + ", deviceDescription=" + deviceDescription + ", status=" + status + + "]"; + } +} diff --git a/src/org/kne/cloud/network/srv6/NodeInfoQueryStatus.java b/src/org/kne/cloud/network/srv6/NodeInfoQueryStatus.java new file mode 100644 index 0000000..5f7d575 --- /dev/null +++ b/src/org/kne/cloud/network/srv6/NodeInfoQueryStatus.java @@ -0,0 +1,6 @@ +package org.kne.cloud.network.srv6; + +public enum NodeInfoQueryStatus { + OK, + DENIED +} diff --git a/src/org/kne/cloud/network/srv6/NodeProfile.java b/src/org/kne/cloud/network/srv6/NodeProfile.java new file mode 100644 index 0000000..7748b11 --- /dev/null +++ b/src/org/kne/cloud/network/srv6/NodeProfile.java @@ -0,0 +1,44 @@ +package org.kne.cloud.network.srv6; + +import java.util.Objects; + +public final class NodeProfile { + private final String deviceName; + private final String deviceDescription; + + public NodeProfile(String deviceName, String deviceDescription) { + this.deviceName = deviceName; + this.deviceDescription = deviceDescription; + } + + public String getDeviceName() { + return deviceName; + } + + public String getDeviceDescription() { + return deviceDescription; + } + + @Override + public int hashCode() { + return Objects.hash(deviceName, deviceDescription); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof NodeProfile)) { + return false; + } + NodeProfile other = (NodeProfile) obj; + return Objects.equals(deviceName, other.deviceName) + && Objects.equals(deviceDescription, other.deviceDescription); + } + + @Override + public String toString() { + return "NodeProfile [deviceName=" + deviceName + ", deviceDescription=" + deviceDescription + "]"; + } +} -- 2.39.5 From dde54d0929e55fb3b7b8b52dc95c3d4f42ab1b6d Mon Sep 17 00:00:00 2001 From: SerinaNya <34389622+SerinaNya@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:18:38 +0800 Subject: [PATCH 4/4] Revert "fix(config): sync Swing settings after Web updates" This reverts commit b6acc6d50999480ad78e07beaac5dc33871c2ec3. --- AGENTS.md | 4 +- src/klalb_en_US.properties | 8 +- src/klalb_zh_CN.properties | 8 +- .../cloud/network/klalb/KLALBController.java | 58 +-- .../cloud/network/klalb/KLALBProxySystem.java | 331 +++--------------- .../network/klalb/ui/KLALBStateGUI3.java | 246 +++---------- .../network/klalb/web/KLALBWebServer.java | 53 +-- 7 files changed, 131 insertions(+), 577 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 56cdb69..859c5a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,8 +32,8 @@ KLALB is a Java SRv6/load-balancing system. Start at `org.kne.cloud.network.klal ## Important Boundaries - `KLALBConfigItem` is a polymorphic JSON array keyed by case-sensitive `Type`. New types need serializer and deserializer support; preserve unknown items' raw JSON. -- `/api/config` parses fields and legacy aliases explicitly. Web and Swing writes must use `KLALBProxySystem`'s revision-checked detached-candidate commit path, never mutate the canonical config directly. -- Vendored Gson is 2.1: serialize HTTP `JsonElement` values with `toString()`; serialize configuration through the configured pretty-print Gson path. +- `/api/config` is field-by-field parsing, not whole-object Gson mapping. Keep legacy key aliases in sync with new fields. +- Vendored Gson is `2.1`: responses that are `JsonElement` instances must be serialized with `JsonElement.toString()`, not reflective `gson.toJson(Object)`. - UI strings use `UIEnv.getRsb()`; add keys to both `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`. - `KLALBController.PublishedNodeInfo` is the thread-safe node-info source. Publish profile, effective external endpoints, and extra routes through the controller so snapshots and update flags remain coherent. Peer queries are separate profile, endpoint, and extra-route requests; use `NodeInfoQueryCoordinator` when a consumer needs an aggregate detail result. - `RouterInfo` retains an empty legacy UTF slot. `RouterInfoPacket` appends optional update flags behind a marker; treat binary codec changes as mesh-compatibility work and preserve old-reader behavior. diff --git a/src/klalb_en_US.properties b/src/klalb_en_US.properties index cf98436..02a5e59 100644 --- a/src/klalb_en_US.properties +++ b/src/klalb_en_US.properties @@ -34,13 +34,7 @@ addline=Add line reconnectall=Reconnect All remotelines=Remote lines settings=Settings -saveconfigsuccess=Save config success -configexternalupdate=The configuration was updated by another source. -configreload=Reload -configcontinue=Continue editing -configsaveconflict=Save failed: the configuration was updated. Reload and try again. -configsavefailed=Failed to save configuration. -configconflicttitle=Configuration conflict +saveconfigsuccess=Save config success warning=Warning invaildipv6addr=IPv6 address:Invaild Input invailddnsserver=DNS server:Invaild Input diff --git a/src/klalb_zh_CN.properties b/src/klalb_zh_CN.properties index ab6b6d5..fc1b624 100644 --- a/src/klalb_zh_CN.properties +++ b/src/klalb_zh_CN.properties @@ -34,13 +34,7 @@ addline=添加链路 reconnectall=全部重连 remotelines=远程链路 settings=设置 -saveconfigsuccess=保存配置成功 -configexternalupdate=配置已被其他来源更新。 -configreload=重新加载 -configcontinue=继续编辑 -configsaveconflict=保存失败:配置已被更新,请重新加载后再试。 -configsavefailed=保存配置失败。 -configconflicttitle=配置冲突 +saveconfigsuccess=保存配置成功 warning=警告 invaildipv6addr=IPv6地址:非法输入 invailddnsserver=DNS服务器:非法输入 diff --git a/src/org/kne/cloud/network/klalb/KLALBController.java b/src/org/kne/cloud/network/klalb/KLALBController.java index cd1460a..1841de3 100644 --- a/src/org/kne/cloud/network/klalb/KLALBController.java +++ b/src/org/kne/cloud/network/klalb/KLALBController.java @@ -583,63 +583,7 @@ public class KLALBController { private KLALBRoutingProtocolAPIClient apiClient; - private volatile KLALBControllerConfigItem configItem; - - private static List copyConfigList(List values) { - return values == null ? null : new ArrayList(values); - } - - private static KLALBControllerConfigItem copyConfigItem(KLALBControllerConfigItem source) { - KLALBControllerConfigItem copy = new KLALBControllerConfigItem(); - copy.setLanguage(source.getLanguage()); - copy.setNogui(source.isNogui()); - copy.setVirtualAddress(source.getVirtualAddress()); - copy.setVirtualASN(source.getVirtualASN()); - copy.setDNS(copyConfigList(source.getDNS())); - copy.setTCPListen(source.getTCPListen()); - copy.setUDPListen(source.getUDPListen()); - copy.setVirtualSocketName(source.getVirtualSocketName()); - copy.setExternalEndpoints(copyConfigList(source.getExternalEndpoints())); - copy.setAutoConnections(copyConfigList(source.getAutoConnections())); - copy.setNtpServers(copyConfigList(source.getNtpServers())); - copy.setExtraRoutes(copyConfigList(source.getExtraRoutes())); - copy.setDenyExternalEndpointQuery(source.isDenyExternalEndpointQuery()); - copy.setDenyExternalEndpointBroadcast(source.isDenyExternalEndpointBroadcast()); - copy.setCongestionAlgorithm(source.getCongestionAlgorithm()); - copy.setBurstLimit(source.getBurstLimit()); - copy.setDelayUpperBound(source.getDelayUpperBound()); - copy.setDelayLowerBound(source.getDelayLowerBound()); - copy.setNagleDelayTime(source.getNagleDelayTime()); - copy.setLinkNagleDelayTime(source.getLinkNagleDelayTime()); - copy.setLinkConnectionsCount(source.getLinkConnectionsCount()); - copy.setEnableTUN(source.isEnableTUN()); - copy.setTUNName(source.getTUNName()); - copy.setPerformanceStrategy(source.getPerformanceStrategy()); - copy.setDeviceName(source.getDeviceName()); - copy.setDeviceDescription(source.getDeviceDescription()); - copy.setNetworkInterfaceExcepts(copyConfigList(source.getNetworkInterfaceExcepts())); - copy.setWebUI(source.isWebUI()); - copy.setWebListen(source.getWebListen()); - return copy; - } - - public void applyConfigItem(KLALBControllerConfigItem committedConfigItem) { - if (committedConfigItem == null) { - throw new IllegalArgumentException("Controller configuration is required"); - } - KLALBControllerConfigItem detachedConfigItem = copyConfigItem(committedConfigItem); - synchronized (externalEndpoints) { - configItem = detachedConfigItem; - if (srv6Router != null) { - PerformanceStrategy strategy = PerformanceStrategy.fromDescription(detachedConfigItem.getPerformanceStrategy()); - if (strategy != null) { - srv6Router.setPerformanceStrategy(strategy); - } - } - publishNodeInfoLocked(detachedConfigItem.getDeviceName(), detachedConfigItem.getDeviceDescription(), - detachedConfigItem.getExternalEndpoints(), detachedConfigItem.getExtraRoutes()); - } - } + private KLALBControllerConfigItem configItem; public void addRemoteLines(List select) { for (MultiProtocolSocketAddress target : select) { diff --git a/src/org/kne/cloud/network/klalb/KLALBProxySystem.java b/src/org/kne/cloud/network/klalb/KLALBProxySystem.java index e17d8d6..85dda3c 100644 --- a/src/org/kne/cloud/network/klalb/KLALBProxySystem.java +++ b/src/org/kne/cloud/network/klalb/KLALBProxySystem.java @@ -4,21 +4,11 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; -import java.io.Reader; -import java.lang.reflect.Type; -import java.net.InetAddress; +import java.io.Reader; +import java.lang.reflect.Type; +import java.net.InetAddress; import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.function.Consumer; +import java.util.HashSet; import org.kne.cloud.network.*; import org.kne.cloud.network.klalb.ui.KLALBStateGUI3; @@ -29,9 +19,8 @@ import org.kne.cloud.network.klalb.web.KLALBWebServer; import java.util.Set; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; @@ -40,82 +29,13 @@ import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.JsonParser; -public class KLALBProxySystem { - public enum ConfigChangeSource { - WEB, SWING - } - - public static final class ControllerConfigSnapshot { - private final long revision; - private final String controllerJson; - - private ControllerConfigSnapshot(long revision, String controllerJson) { - this.revision = revision; - this.controllerJson = controllerJson; - } - - public long getRevision() { - return revision; - } - - public String getControllerJson() { - return controllerJson; - } - } - - public static final class ConfigChangeEvent { - private final ControllerConfigSnapshot snapshot; - private final ConfigChangeSource source; - - private ConfigChangeEvent(ControllerConfigSnapshot snapshot, ConfigChangeSource source) { - this.snapshot = snapshot; - this.source = source; - } - - public ControllerConfigSnapshot getSnapshot() { - return snapshot; - } - - public ConfigChangeSource getSource() { - return source; - } - } - - public static final class CommitResult { - private final boolean success; - private final long revision; - private final ControllerConfigSnapshot snapshot; - - private CommitResult(boolean success, long revision, ControllerConfigSnapshot snapshot) { - this.success = success; - this.revision = revision; - this.snapshot = snapshot; - } - - public boolean isSuccess() { - return success; - } - - public long getRevision() { - return revision; - } - - public ControllerConfigSnapshot getSnapshot() { - return snapshot; - } - } - - private Set proxys=new HashSet<>(); - private KLALBController klalbController; - private KLALBWebServer webServer; - private KLALBConfig config; - private Gson gson; - private File jsonFile; - private final Object configLock = new Object(); - private long configRevision; - private volatile ControllerConfigSnapshot controllerConfigSnapshot = new ControllerConfigSnapshot(0L, null); - private final CopyOnWriteArraySet> configChangeListeners = new CopyOnWriteArraySet<>(); - private JsonArray rawConfigJson; +public class KLALBProxySystem { + private Set proxys=new HashSet<>(); + private KLALBController klalbController; + private KLALBWebServer webServer; + private KLALBConfig config; + private Gson gson; + private File jsonFile; { GsonBuilder gb=new GsonBuilder().setPrettyPrinting(); MultiProtocolSocketAddress.registerToGsonBuilder(gb); @@ -221,27 +141,13 @@ public class KLALBProxySystem { public void loadConfigJson(Reader json) { loadConfigJson(new JsonParser().parse(json)); } - public void loadConfigJson(JsonElement json) { - JsonElement rawJson = new JsonParser().parse(json.toString()); - KLALBConfig config= gson.fromJson(json, KLALBConfig.class); - JsonArray rawArray = rawJson instanceof JsonArray ? (JsonArray) rawJson : null; - synchronized (configLock) { - installConfigLocked(config, rawArray); - } - } - public void loadConfig(KLALBConfig config) { - JsonElement rawJson = gson.toJsonTree(config); - JsonArray rawArray = rawJson instanceof JsonArray ? (JsonArray) rawJson : null; - synchronized (configLock) { - installConfigLocked(config, rawArray); - } - } - - private void installConfigLocked(KLALBConfig config, JsonArray rawArray) { - this.config=config; - this.configRevision=0L; - rawConfigJson=rawArray; - for(KLALBConfigItem item:config) { + public void loadConfigJson(JsonElement json) { + KLALBConfig config= gson.fromJson(json, KLALBConfig.class); + loadConfig(config); + } + public void loadConfig(KLALBConfig config) { + this.config=config; + for(KLALBConfigItem item:config) { if(item instanceof KLALBControllerConfigItem) { KLALBControllerConfigItem kcci=(KLALBControllerConfigItem) item; String lstr=kcci.getLanguage(); @@ -321,180 +227,41 @@ public class KLALBProxySystem { } catch (IOException e) { e.printStackTrace(); } - } - } - controllerConfigSnapshot = createControllerConfigSnapshotLocked(); - } + } + } + } - public void saveConfigToFile() { - synchronized (configLock) { - if (jsonFile != null && config != null) { - try { - KLALBControllerConfigItem item = findControllerConfigItemLocked(); - if (item == null) { - persistConfigJsonLocked(gson.toJson(config)); - } else { - String controllerJson = gson.toJson(item); - JsonArray completeConfig = createCompleteConfigJsonLocked(controllerJson); - persistConfigJsonLocked(gson.toJson(completeConfig)); - rawConfigJson = completeConfig; - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - private KLALBControllerConfigItem findControllerConfigItemLocked() { - if (config == null) { - return null; - } - for (KLALBConfigItem item : config) { - if (item instanceof KLALBControllerConfigItem) { - return (KLALBControllerConfigItem) item; - } - } - return null; - } - - private int findControllerConfigIndexLocked() { - if (config == null) { - return -1; - } - for (int i = 0; i < config.size(); i++) { - if (config.get(i) instanceof KLALBControllerConfigItem) { - return i; - } - } - return -1; - } - - private ControllerConfigSnapshot createControllerConfigSnapshotLocked() { - KLALBControllerConfigItem item = findControllerConfigItemLocked(); - return new ControllerConfigSnapshot(configRevision, item == null ? null : gson.toJson(item)); - } - - public ControllerConfigSnapshot getControllerConfigSnapshot() { - synchronized (configLock) { - controllerConfigSnapshot = createControllerConfigSnapshotLocked(); - return controllerConfigSnapshot; - } - } - - public KLALBControllerConfigItem parseControllerConfigCandidate(ControllerConfigSnapshot snapshot) { - if (snapshot == null || snapshot.getControllerJson() == null) { - return null; - } - return gson.fromJson(snapshot.getControllerJson(), KLALBControllerConfigItem.class); - } - - private void persistConfigJsonLocked(String json) throws IOException { - if (jsonFile == null) { - return; - } - Path target = jsonFile.toPath().toAbsolutePath(); - Path parent = target.getParent(); - Path temporary = Files.createTempFile(parent, target.getFileName().toString(), ".tmp"); - try { - Files.write(temporary, json.getBytes(StandardCharsets.UTF_8), StandardOpenOption.TRUNCATE_EXISTING, - StandardOpenOption.WRITE); - try { - Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } catch (AtomicMoveNotSupportedException e) { - Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING); - } - } finally { - Files.deleteIfExists(temporary); - } - } - - private JsonArray createCompleteConfigJsonLocked(String controllerJson) throws IOException { - JsonElement source = rawConfigJson == null ? gson.toJsonTree(config) : rawConfigJson; - if (!(source instanceof JsonArray)) { - throw new IOException("Configuration is not an array"); - } - JsonArray sourceArray = (JsonArray) source; - int index = findControllerConfigIndexLocked(); - if (index < 0 || index >= sourceArray.size() || sourceArray.size() != config.size()) { - throw new IOException("Configuration item layout changed"); - } - JsonArray mergedConfig = new JsonArray(); - JsonElement controllerElement = new JsonParser().parse(controllerJson); - for (int i = 0; i < sourceArray.size(); i++) { - mergedConfig.add(i == index ? controllerElement : sourceArray.get(i)); - } - return mergedConfig; - } - - public void addConfigChangeListener(Consumer listener) { - configChangeListeners.add(listener); - } - - public void removeConfigChangeListener(Consumer listener) { - configChangeListeners.remove(listener); - } - - public CommitResult commitControllerConfig(long expectedRevision, KLALBControllerConfigItem candidate, - ConfigChangeSource source) throws IOException { - if (candidate == null || source == null) { - throw new IllegalArgumentException("Candidate and source are required"); - } - ConfigChangeEvent event; - CommitResult result; - synchronized (configLock) { - if (expectedRevision != configRevision) { - ControllerConfigSnapshot currentSnapshot = createControllerConfigSnapshotLocked(); - controllerConfigSnapshot = currentSnapshot; - return new CommitResult(false, configRevision, currentSnapshot); - } - int index = findControllerConfigIndexLocked(); - if (index < 0) { - throw new IOException("Controller configuration is missing"); - } - String candidateJson = gson.toJson(candidate); - KLALBControllerConfigItem committedCandidate = gson.fromJson(candidateJson, - KLALBControllerConfigItem.class); - if (PerformanceStrategy.fromDescription(committedCandidate.getPerformanceStrategy()) == null) { - throw new IllegalArgumentException("Unknown performanceStrategy: " - + committedCandidate.getPerformanceStrategy()); - } - com.google.gson.JsonArray mergedConfig = createCompleteConfigJsonLocked(candidateJson); - persistConfigJsonLocked(gson.toJson(mergedConfig)); - rawConfigJson = mergedConfig; - - config.set(index, committedCandidate); - if (klalbController != null) { - klalbController.applyConfigItem(committedCandidate); - } - configRevision++; - controllerConfigSnapshot = createControllerConfigSnapshotLocked(); - event = new ConfigChangeEvent(controllerConfigSnapshot, source); - result = new CommitResult(true, configRevision, controllerConfigSnapshot); - } - for (Consumer listener : configChangeListeners) { - try { - listener.accept(event); - } catch (Throwable e) { - e.printStackTrace(); - } - } - return result; - } + 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; - public KLALBStateGUI3 getKLALBGUI() { - if(kgui==null) { - kgui=new KLALBStateGUI3(klalbController); - kgui.bindConfigSystem(this); - } + public KLALBStateGUI3 getKLALBGUI() { + if(kgui==null) { + kgui=new KLALBStateGUI3(klalbController); + kgui.loadConfig(config); + kgui.setSaveComsumer((cfg)->{ + saveConfigToFile(); + }); + } return kgui; } public KLALBConfig getConfig() { return config; } - public KLALBControllerConfigItem getControllerConfig() { - return parseControllerConfigCandidate(getControllerConfigSnapshot()); - } -} + public KLALBControllerConfigItem getControllerConfig() { + for(KLALBConfigItem item:config) { + if(item instanceof KLALBControllerConfigItem) + return (KLALBControllerConfigItem) item; + } + return null; + } +} diff --git a/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java b/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java index c72a353..b110b94 100644 --- a/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java +++ b/src/org/kne/cloud/network/klalb/ui/KLALBStateGUI3.java @@ -104,82 +104,8 @@ public class KLALBStateGUI3 extends XFrame { private JCheckBox nogui; // ==================== 配置和回调 ==================== - private KLALBConfig config; // 配置文件 - private Consumer saveComsumer; // 保存配置的回调函数 - private KLALBProxySystem configSystem; - private KLALBProxySystem.ControllerConfigSnapshot loadedSnapshot; - private long loadedRevision; - private SettingsFormState loadedFormState; - private KLALBProxySystem.ControllerConfigSnapshot pendingExternalSnapshot; - private long pendingExternalRevision; - private boolean configSystemClosed; - private long bindingGeneration; - private Consumer boundConfigChangeListener; - - private static final class SettingsFormState { - private final String language, deviceName, deviceDescription, address, dns, extraRoutes; - private final String asn, tunName, webListen, tcpListen, udpListen, openLines, connectLines, ntp; - private final String performance, congestion; - private final boolean enableTun, webApi, nogui, denyQuery, denyBroadcast; - private final List interfaces; - private final int connections, burst, upper, lower, nagle, linkNagle; - - private SettingsFormState(KLALBStateGUI3 gui) { - Language languageItem = (Language) gui.comboLang.getSelectedItem(); - language = languageItem == null ? null : languageItem.name(); - deviceName = gui.deviceNameSet.getText(); - deviceDescription = gui.deviceDescriptionSet.getText(); - address = gui.addressFieldSet.getText(); - dns = gui.dnsAreaSet.getText(); - extraRoutes = gui.extraRoutesSet.getText(); - asn = gui.asnFieldSet.getText(); - tunName = gui.tunDeviceName.getText(); - webListen = gui.webListenSet.getText(); - tcpListen = gui.tcpListeningSet.getText(); - udpListen = gui.udpListeningSet.getText(); - openLines = gui.openLineTabelSet.getText(); - connectLines = gui.connectLineTabelSet.getText(); - ntp = gui.ntpServerSet.getText(); - PerformanceStrategyItem performanceItem = (PerformanceStrategyItem) gui.comboPerformance.getSelectedItem(); - performance = performanceItem == null ? null : performanceItem.getStrategy().toString(); - congestion = String.valueOf(gui.congestions.getComboBox().getSelectedItem()); - enableTun = gui.enableTUN.isSelected(); - webApi = gui.webApiEnabled.isSelected(); - nogui = gui.nogui.isSelected(); - denyQuery = gui.denyQuery.isSelected(); - denyBroadcast = gui.denyBroadcast.isSelected(); - interfaces = new ArrayList<>(); - for (int i = 0; i < gui.nilsimdl.getSize(); i++) interfaces.add(gui.nilsimdl.getElementAt(i).getName()); - connections = gui.linkConnectionsCount.getSlider().getValue(); - burst = gui.burstLimit.getSlider().getValue(); - upper = gui.delayHbound.getSlider().getValue(); - lower = gui.delayLbound.getSlider().getValue(); - nagle = gui.nagleDelayTime.getSlider().getValue(); - linkNagle = gui.linkNagleDelayTime.getSlider().getValue(); - } - - @Override public boolean equals(Object obj) { - if (!(obj instanceof SettingsFormState)) return false; - SettingsFormState o = (SettingsFormState) obj; - return enableTun == o.enableTun && webApi == o.webApi && nogui == o.nogui - && denyQuery == o.denyQuery && denyBroadcast == o.denyBroadcast - && connections == o.connections && burst == o.burst && upper == o.upper - && lower == o.lower && nagle == o.nagle && linkNagle == o.linkNagle - && Objects.equals(language, o.language) && Objects.equals(deviceName, o.deviceName) - && Objects.equals(deviceDescription, o.deviceDescription) && Objects.equals(address, o.address) - && Objects.equals(dns, o.dns) && Objects.equals(extraRoutes, o.extraRoutes) - && Objects.equals(asn, o.asn) && Objects.equals(tunName, o.tunName) - && Objects.equals(webListen, o.webListen) && Objects.equals(tcpListen, o.tcpListen) - && Objects.equals(udpListen, o.udpListen) && Objects.equals(openLines, o.openLines) - && Objects.equals(connectLines, o.connectLines) && Objects.equals(ntp, o.ntp) - && Objects.equals(performance, o.performance) && Objects.equals(congestion, o.congestion) - && Objects.equals(interfaces, o.interfaces); - } - @Override public int hashCode() { return Objects.hash(language, deviceName, deviceDescription, address, dns, - extraRoutes, asn, tunName, webListen, tcpListen, udpListen, openLines, connectLines, ntp, - performance, congestion, enableTun, webApi, nogui, denyQuery, denyBroadcast, interfaces, - connections, burst, upper, lower, nagle, linkNagle); } - } + private KLALBConfig config; // 配置文件 + private Consumer saveComsumer; // 保存配置的回调函数 // ==================== 尺寸常量 ==================== private Dimension dashSize = new Dimension((int) (145 * 0.7), (int) (165 * 0.7)); // 仪表盘尺寸 @@ -1129,10 +1055,15 @@ public class KLALBStateGUI3 extends XFrame { /** * 保存配置到文件 */ - private void saveConfig() { - if (configSystem == null || loadedSnapshot == null) return; - KLALBControllerConfigItem kck = configSystem.parseControllerConfigCandidate(loadedSnapshot); - if (kck == null) return; + private void saveConfig() { + if (config == null) { + config = new KLALBConfig(); + config.add(new KLALBControllerConfigItem()); + } + + for (KLALBConfigItem item : config) { + if (item instanceof KLALBControllerConfigItem) { + KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item; String oldDeviceName = kck.getDeviceName(); String oldDeviceDescription = kck.getDeviceDescription(); List oldExternalEndpoints = kck.getExternalEndpoints() == null @@ -1383,32 +1314,30 @@ public class KLALBStateGUI3 extends XFrame { kck.setLinkNagleDelayTime(linkNagleDelayTime.getSlider().getValue()*100000L); - kck.setDeviceName(newDeviceName); - kck.setDeviceDescription(newDeviceDescription); - kck.setExternalEndpoints(newExternalEndpoints); - kck.setExtraRoutes(newExtraRoutes); - try { - KLALBProxySystem.CommitResult result = configSystem.commitControllerConfig(loadedRevision, kck, - KLALBProxySystem.ConfigChangeSource.SWING); - if (!result.isSuccess()) { - resolveConfigConflict(result.getSnapshot(), "configsaveconflict"); - return; - } - loadedSnapshot = result.getSnapshot(); - loadedRevision = result.getRevision(); - loadedFormState = new SettingsFormState(this); - pendingExternalSnapshot = null; - JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("saveconfigsuccess")); - } catch (IOException e) { - e.printStackTrace(); - JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("configsavefailed"), - UIEnv.getRsb().getString("warning"), JOptionPane.WARNING_MESSAGE); - } catch (RuntimeException e) { - e.printStackTrace(); - JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("configsavefailed"), - UIEnv.getRsb().getString("warning"), JOptionPane.WARNING_MESSAGE); - } - } + boolean deviceNameChanged = !Objects.equals(oldDeviceName, newDeviceName); + boolean deviceDescriptionChanged = !Objects.equals(oldDeviceDescription, newDeviceDescription); + boolean externalEndpointsChanged = !Objects.equals(oldExternalEndpoints, newExternalEndpoints); + boolean extraRoutesChanged = !Objects.equals(oldExtraRoutes, newExtraRoutes); + if (deviceNameChanged || deviceDescriptionChanged || externalEndpointsChanged || extraRoutesChanged) { + if (kcontroller != null) { + kcontroller.publishNodeInfo(newDeviceName, newDeviceDescription, newExternalEndpoints, + newExtraRoutes); + } else { + kck.setDeviceName(newDeviceName); + kck.setDeviceDescription(newDeviceDescription); + kck.setExternalEndpoints(newExternalEndpoints); + kck.setExtraRoutes(newExtraRoutes); + } + } + } + } + + // 调用保存回调 + if (saveComsumer != null) { + saveComsumer.accept(config); + JOptionPane.showMessageDialog(this, UIEnv.getRsb().getString("saveconfigsuccess")); + } + } // ==================== 刷新任务创建 ==================== /** @@ -1636,7 +1565,11 @@ public class KLALBStateGUI3 extends XFrame { /** * 加载配置文件 */ - private void loadConfigCandidate(KLALBControllerConfigItem kck) { + public void loadConfig(KLALBConfig config) { + this.config = config; + for (KLALBConfigItem item : config) { + if (item instanceof KLALBControllerConfigItem) { + KLALBControllerConfigItem kck = (KLALBControllerConfigItem) item; // 加载语言设置 String lg = kck.getLanguage(); @@ -1762,92 +1695,9 @@ public class KLALBStateGUI3 extends XFrame { long delr=kck.getLinkNagleDelayTime(); linkNagleDelayTime.getSlider().setValue((int)(delr/100000L)); - } - - public void loadConfig(KLALBConfig config) { - if (configSystem != null) return; - this.config = config; - if (config == null) return; - for (KLALBConfigItem item : config) { - if (item instanceof KLALBControllerConfigItem) { - loadConfigCandidate((KLALBControllerConfigItem) item); - break; - } - } - } - - public void bindConfigSystem(KLALBProxySystem system) { - if (configSystemClosed || system == null) return; - final long generation = ++bindingGeneration; - Runnable bind = () -> { - if (configSystemClosed || generation != bindingGeneration) return; - if (configSystem != null && boundConfigChangeListener != null) - configSystem.removeConfigChangeListener(boundConfigChangeListener); - configSystem = system; - final Consumer listener = event -> - SwingUtilities.invokeLater(() -> handleConfigChange(system, generation, event)); - boundConfigChangeListener = listener; - pendingExternalSnapshot = null; - pendingExternalRevision = 0L; - loadedSnapshot = null; - loadedRevision = 0L; - loadedFormState = null; - system.addConfigChangeListener(listener); - loadedSnapshot = system.getControllerConfigSnapshot(); - KLALBControllerConfigItem candidate = system.parseControllerConfigCandidate(loadedSnapshot); - if (candidate != null) { - loadConfigCandidate(candidate); - loadedRevision = loadedSnapshot.getRevision(); - loadedFormState = new SettingsFormState(this); - } - }; - if (SwingUtilities.isEventDispatchThread()) bind.run(); - else SwingUtilities.invokeLater(bind); - } - - private void handleConfigChange(KLALBProxySystem sourceSystem, long generation, - KLALBProxySystem.ConfigChangeEvent event) { - if (configSystemClosed || generation != bindingGeneration || sourceSystem != configSystem - || event.getSource() == KLALBProxySystem.ConfigChangeSource.SWING) return; - KLALBProxySystem.ControllerConfigSnapshot snapshot = event.getSnapshot(); - if (snapshot == null || snapshot.getRevision() <= Math.max(loadedRevision, pendingExternalRevision)) return; - if (loadedFormState != null && loadedFormState.equals(new SettingsFormState(this))) { - applyExternalSnapshot(snapshot); - return; - } - if (pendingExternalSnapshot == null || snapshot.getRevision() > pendingExternalRevision) { - boolean hadPending = pendingExternalSnapshot != null; - pendingExternalSnapshot = snapshot; - pendingExternalRevision = snapshot.getRevision(); - if (!hadPending) resolveConfigConflict(snapshot, "configexternalupdate"); - } - } - - private void resolveConfigConflict(KLALBProxySystem.ControllerConfigSnapshot snapshot, String messageKey) { - if (snapshot != null && snapshot.getRevision() > Math.max(loadedRevision, pendingExternalRevision)) { - pendingExternalSnapshot = snapshot; - pendingExternalRevision = snapshot.getRevision(); - } - Object[] options = { UIEnv.getRsb().getString("configreload"), UIEnv.getRsb().getString("configcontinue") }; - int choice = JOptionPane.showOptionDialog(this, UIEnv.getRsb().getString(messageKey), - UIEnv.getRsb().getString("configconflicttitle"), JOptionPane.DEFAULT_OPTION, - JOptionPane.WARNING_MESSAGE, null, options, options[0]); - if (choice == 0 && pendingExternalSnapshot != null - && pendingExternalSnapshot.getRevision() >= loadedRevision) { - applyExternalSnapshot(pendingExternalSnapshot); - } - } - - private void applyExternalSnapshot(KLALBProxySystem.ControllerConfigSnapshot snapshot) { - KLALBControllerConfigItem candidate = configSystem.parseControllerConfigCandidate(snapshot); - if (candidate == null) return; - loadConfigCandidate(candidate); - loadedSnapshot = snapshot; - loadedRevision = snapshot.getRevision(); - loadedFormState = new SettingsFormState(this); - pendingExternalSnapshot = null; - pendingExternalRevision = 0L; - } + } + } + } // ==================== 辅助方法 ==================== /** @@ -1922,9 +1772,8 @@ public class KLALBStateGUI3 extends XFrame { /** * 设置保存配置的回调函数 */ - public void setSaveComsumer(Consumer saveComsumer) { - if (configSystem != null) return; - this.saveComsumer = saveComsumer; + public void setSaveComsumer(Consumer saveComsumer) { + this.saveComsumer = saveComsumer; } /** @@ -1939,11 +1788,6 @@ public class KLALBStateGUI3 extends XFrame { * 关闭窗口并清理资源 */ public void close() { - bindingGeneration++; - configSystemClosed = true; - if (configSystem != null && boundConfigChangeListener != null) - configSystem.removeConfigChangeListener(boundConfigChangeListener); - boundConfigChangeListener = null; for (int i = 0; i < tabbedPane.getTabCount(); i++) { Component component = tabbedPane.getComponentAt(i); if (component instanceof NodeInformationPanel) { diff --git a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java index 63a411f..c9bc0b3 100644 --- a/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java +++ b/src/org/kne/cloud/network/klalb/web/KLALBWebServer.java @@ -863,11 +863,17 @@ public class KLALBWebServer { String body = readRequestBody(exchange); try { JsonObject json = new JsonParser().parse(body).getAsJsonObject(); - KLALBProxySystem.ControllerConfigSnapshot baseSnapshot = proxySystem.getControllerConfigSnapshot(); - KLALBControllerConfigItem current = proxySystem.parseControllerConfigCandidate(baseSnapshot); + KLALBControllerConfigItem current = proxySystem.getControllerConfig(); if (current != null) { - String newDeviceName = current.getDeviceName(); - String newDeviceDescription = current.getDeviceDescription(); + String oldDeviceName = current.getDeviceName(); + String oldDeviceDescription = current.getDeviceDescription(); + List oldExternalEndpoints = current.getExternalEndpoints() == null + ? null + : new ArrayList(current.getExternalEndpoints()); + List oldExtraRoutes = current.getExtraRoutes() == null + ? null : new ArrayList(current.getExtraRoutes()); + String newDeviceName = oldDeviceName; + String newDeviceDescription = oldDeviceDescription; List newExternalEndpoints = current.getExternalEndpoints(); List newExtraRoutes = current.getExtraRoutes(); if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) { @@ -1066,32 +1072,37 @@ public class KLALBWebServer { current.setDenyExternalEndpointBroadcast(json.get("denyLineTableBroadcast").getAsBoolean()); } - current.setDeviceName(newDeviceName); - current.setDeviceDescription(newDeviceDescription); - current.setExternalEndpoints(newExternalEndpoints); - current.setExtraRoutes(newExtraRoutes); - KLALBProxySystem.CommitResult commitResult = proxySystem.commitControllerConfig( - baseSnapshot.getRevision(), current, - KLALBProxySystem.ConfigChangeSource.WEB); - if (!commitResult.isSuccess()) { - JsonObject conflict = new JsonObject(); - conflict.addProperty("success", false); - conflict.addProperty("revision", commitResult.getRevision()); - conflict.addProperty("error", "Configuration revision conflict"); - sendJsonResponse(exchange, 409, conflict); - return; + boolean deviceNameChanged = !Objects.equals(oldDeviceName, newDeviceName); + boolean deviceDescriptionChanged = !Objects.equals(oldDeviceDescription, newDeviceDescription); + boolean externalEndpointsChanged = !Objects.equals(oldExternalEndpoints, newExternalEndpoints); + boolean extraRoutesChanged = !Objects.equals(oldExtraRoutes, newExtraRoutes); + if (deviceNameChanged || deviceDescriptionChanged || externalEndpointsChanged || extraRoutesChanged) { + KLALBController kc = proxySystem.getKlalbController(); + if (kc != null) { + kc.publishNodeInfo(newDeviceName, newDeviceDescription, newExternalEndpoints, + newExtraRoutes); + } else { + current.setDeviceName(newDeviceName); + current.setDeviceDescription(newDeviceDescription); + current.setExternalEndpoints(newExternalEndpoints); + current.setExtraRoutes(newExtraRoutes); + } + } + + // 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("revision", commitResult.getRevision()); resp.addProperty("message", "Configuration updated successfully"); sendJsonResponse(exchange, 200, resp); } else { sendError(exchange, 500, "Current configuration is null"); } - } catch (IOException e) { - sendError(exchange, 500, "Failed to persist configuration: " + e.getMessage()); } catch (Exception e) { sendError(exchange, 400, "Failed to update configuration: " + e.getMessage()); } -- 2.39.5