diff --git a/AGENTS.md b/AGENTS.md index 16377ee..859c5a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,101 +1,39 @@ -# 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. Start at `org.kne.cloud.network.klalb.KLALBMain`; runtime configuration is root-relative `klalb-config.json`. -## 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 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) ``` -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. `src` must stay on the classpath because manual compilation does not copy resource bundles: ```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. +- 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. -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 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 -No test suite, no CI. Classes named `*Test*` (`nathole/`, `ntp/`) are manual `main()` harnesses requiring real network peers. Practical check = compile succeeds + app launches. +- 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`. -## 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 case-sensitive `Type`. New types need serializer and deserializer support; preserve unknown items' raw JSON. +- `/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/dashboard b/dashboard index 89474b9..8c13af6 160000 --- a/dashboard +++ b/dashboard @@ -1 +1 @@ -Subproject commit 89474b9e91791b8efe098a13171e6ff962ad7ba1 +Subproject commit 8c13af680a88f38abc033144e1c273b9ab3e2117 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..1841de3 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; @@ -51,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; @@ -76,7 +79,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 +171,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 +184,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 +194,15 @@ public class KLALBController { } } - } - - lineslock.writeLock().lock(); + } + synchronized (externalEndpoints) { + if (!discoveredExternalEndpoints.equals(currentDiscoveredExternalEndpoints)) { + discoveredExternalEndpoints = currentDiscoveredExternalEndpoints; + publishDiscoveredExternalEndpointLocked(); + } + } + + lineslock.writeLock().lock(); try { @@ -298,8 +349,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) { @@ -339,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); } @@ -401,9 +454,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 +750,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 +911,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 +951,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/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/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..6b0de51 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.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 { - 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 requestNodeProfile(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.requestNodeProfile( + new InetSocketAddress(address.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), + (profile) -> SwingUtilities.invokeLater(() -> handleNodeProfile(address, requestGeneration, profile))); + } catch (IOException e) { + synchronized (nodeInfoLock) { + if (nodeInfoRequestGenerations.get(address) != null + && nodeInfoRequestGenerations.get(address).longValue() == requestGeneration) { + nodeInfoRequestTimes.remove(address); + nodeInfoRequestGenerations.remove(address); + } + } + } + } + + private void handleNodeProfile(IPv6Address address, long requestGeneration, NodeProfile profile) { + if (nodeInfoClosed) + return; + boolean currentRequest; + boolean nodeExists = getNodes().containsKey(address); + String deviceName = profile == null ? "" : profile.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(); + requestNodeProfile(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)) + requestNodeProfile(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..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; @@ -15,33 +15,42 @@ 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; 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; 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; - private KLALBRoutingProtocolAPIClient client; + private IPv6Address address; + private KLALBRoutingProtocolAPIClient client; + private NodeInfoQueryCoordinator queryCoordinator; 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 +82,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 +176,110 @@ 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); + 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) { + requestFullInfo(overviewArea, extraRoutesEmptyLabel); + } + }; + routingProtocol.addNodeInfoUpdateListener(nodeInfoUpdateListener); + requestFullInfo(overviewArea, extraRoutesEmptyLabel); + } + + 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 { + 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; + if (error != null || info == null) { + return; + } + applyRemoteNodeInfo(overviewArea, extraRoutesEmptyLabel, info); + }); + }); + } 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; + 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..c9bc0b3 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.*; @@ -19,12 +20,14 @@ 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; import org.kne.cloud.network.srv6.SRv6Router; /** @@ -35,14 +38,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 +53,36 @@ 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) { + nodeInfoRevisions.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; + 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,159 @@ 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 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 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.requestNodeProfile( + new InetSocketAddress(addr.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT), + profile -> { + synchronized (tinyNameStateLock) { + Long activeRequestGeneration = tinyNameRequestGenerations.get(addr); + if (!Long.valueOf(requestGenerationToken).equals(activeRequestGeneration)) return; + tinyDeviceNameCache.put(addr, + profile != null && profile.getDeviceName() != null ? profile.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,56 +686,70 @@ 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("reachable", true); + 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; } // 远端节点:经 SRv6 虚拟网络发送完整节点信息查询(异步回调,限时等待) resp.addProperty("isSelf", false); - CompletableFuture future = new CompletableFuture<>(); try { - getNodeInfoClient(kc).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; + KLALBRoutingProtocolAPIClient client = getNodeInfoClient(kc); + if (client == null) { + sendError(exchange, 503, "Node info service stopped"); + return; } - - String dname = info != null && info.getDeviceName() != null && !info.getDeviceName().isEmpty() - ? info.getDeviceName() - : router.getKlalbRouteProtol().getDeviceName(target); - resp.addProperty("deviceName", dname != null ? dname : ""); + 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.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 : "")); + } + } + 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 +784,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)); + } + nodeInfoRevisions.keySet().removeIf(address -> !activeAddresses.contains(address)); if (addrs != null) { for (IPv6Address addr : addrs.keySet()) { @@ -592,8 +803,12 @@ 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 nodeInfoRevision = addr.equals(selfAddr) ? published.getFullRevision() + : nodeInfoRevisions.getOrDefault(addr, 0L); + nodeObj.addProperty("nodeInfoRevision", nodeInfoRevision); nodesArray.add(nodeObj); } } @@ -644,21 +859,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 +960,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 +1010,7 @@ public class KLALBWebServer { list.add(el.getAsString()); } } - current.setExtraRoutes(list); + newExtraRoutes = list; } if (json.has("networkInterfaceExcepts") || json.has("NetworkInterfaceExcepts")) { @@ -845,6 +1072,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 +1106,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/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/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 deleted file mode 100644 index d38f5d6..0000000 --- a/src/org/kne/cloud/network/srv6/KLALBNodeInformation.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.kne.cloud.network.srv6; - -import java.util.List; - -import org.kne.cloud.network.MultiProtocolSocketAddress; - -/** - * 节点信息(开放线路 + 设备名称 + 设备描述),由路由协议 JSON API 查询获得。 - */ -public class KLALBNodeInformation { - private List openLines; - private String deviceName; - private String deviceDescription; - - public KLALBNodeInformation(List openLines, String deviceName, - String deviceDescription) { - super(); - this.openLines = openLines; - this.deviceName = deviceName; - this.deviceDescription = deviceDescription; - } - - 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; - } - - @Override - public String toString() { - return "KLALBNodeInformation [openLines=" + openLines + ", deviceName=" + deviceName - + ", deviceDescription=" + deviceDescription + "]"; - } -} 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..07671df 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIClient.java @@ -1,125 +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)); - - } - } - } - // 组装节点信息(线路 + 设备名称 + 设备描述,精简模式下线路与描述为 null) - KLALBNodeInformation info = new KLALBNodeInformation(connectsm, dataobj.getDeviceName(), - dataobj.getDeviceDescription()); - ((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); - } -} \ No newline at end of file +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. + } + } + } + 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 d1c3a4b..01a3cc5 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolAPIServer.java @@ -1,88 +1,98 @@ -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.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 限制) - KLALBRoutingProtocolJsonData tinyjson=new KLALBRoutingProtocolJsonData(KLALBRoutingProtocolJsonData.NODE_INFO_TINY_RESP,dataobj.getUuid(),null); - tinyjson.setDeviceName(controller.getIpv6Router().getDeviceName()); - routingProtocol.sendJsonPacketToAddress(new JsonDataPacket(tinyjson),addr); - break; - case KLALBRoutingProtocolJsonData.NODE_INFO_FULL_REQ: - // 完整查询:设备名称与描述总是正常响应;denyExternalEndpointQuery 仅隐藏外部端点列表 - boolean denyEndpoints=controller.getConfigItem()!=null&&controller.getConfigItem().isDenyExternalEndpointQuery(); - KLALBRoutingProtocolJsonData json=new KLALBRoutingProtocolJsonData(KLALBRoutingProtocolJsonData.NODE_INFO_FULL_RESP,dataobj.getUuid(),denyEndpoints?null:controller.getExternalEndpoints()); - json.setDeviceName(controller.getIpv6Router().getDeviceName());// 附带本机设备名称 - if(controller.getConfigItem()!=null) { - json.setDeviceDescription(controller.getConfigItem().getDeviceDescription());// 附带本机设备描述 - } - 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); - } -} \ No newline at end of file +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.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(); + 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 ca8f1b5..b714a8d 100644 --- a/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java +++ b/src/org/kne/cloud/network/srv6/KLALBRoutingProtocolJsonData.java @@ -1,95 +1,89 @@ -package org.kne.cloud.network.srv6; - -import java.util.UUID; - -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;// 对端设备描述 - 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 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()); - 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)) - 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 + "]"; - } - -} +package org.kne.cloud.network.srv6; + +import java.util.Objects; +import java.util.UUID; + +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 NodeInfoQueryStatus getStatus() { + return status; + } + + public void setStatus(NodeInfoQueryStatus status) { + this.status = status; + } + + @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; + } + 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/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/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 + "]"; + } +} 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; + } } }