Skip to content

Commit baaad95

Browse files
committed
feat: Add diagnostics summary and history APIs, along with Network Insights page for enhanced network monitoring
1 parent 128b59a commit baaad95

10 files changed

Lines changed: 819 additions & 39 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,13 @@ chmod +x install-plugins.sh
7575
./install-plugins.sh
7676

7777
# Build & run
78-
go build
79-
./nettool --port 8080
78+
go build -o nettool .
79+
NETTOOL_ALLOW_DEV_BUILD=1 ./nettool --port 8080
8080
```
8181

82-
> **Pi Zero tip:** Use `env CGO_ENABLED=0 go build` if you hit CGO-related link errors.
82+
> **Development note:** source-built binaries do not match the signed release hashes. For local development/testing, opt in explicitly with `NETTOOL_ALLOW_DEV_BUILD=1`. Official release builds still use the normal integrity verification path.
83+
>
84+
> **Pi Zero tip:** Use `env CGO_ENABLED=0 go build -o nettool .` if you hit CGO-related link errors.
8385
8486
Visit `http://<device-ip>:8080` to open the dashboard.
8587

app/core/integrity.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ var (
2929
IntegrityEnabled = "true"
3030
)
3131

32+
const devIntegrityBypassEnv = "NETTOOL_ALLOW_DEV_BUILD"
33+
3234
// Trusted DNS servers for verification
3335
// We use multiple independent DNS providers to detect DNS spoofing
3436
var trustedDNSServers = []string{
@@ -152,6 +154,19 @@ func VerifyBinaryIntegrity() *IntegrityStatus {
152154
return status
153155
}
154156

157+
// Explicit development-mode escape hatch for source builds.
158+
// This is only available when no embedded production hash is present,
159+
// and it requires an explicit runtime opt-in.
160+
if isTruthyEnv(os.Getenv(devIntegrityBypassEnv)) {
161+
status.Source = "development"
162+
status.Verified = true
163+
status.ShouldBlock = false
164+
status.Error = fmt.Sprintf("development build allowed via %s; production integrity verification remains disabled for this run", devIntegrityBypassEnv)
165+
cachedStatus = status
166+
integrityChecked = true
167+
return status
168+
}
169+
155170
// PRIORITY 2: Fetch hash from GitHub releases (remote trusted source)
156171
// This is the ONLY external source we trust - NOT local files
157172
githubHash, source, err := fetchHashFromGitHub()
@@ -197,6 +212,15 @@ func GetCachedIntegrityStatus() *IntegrityStatus {
197212
return cachedStatus
198213
}
199214

215+
func isTruthyEnv(value string) bool {
216+
switch strings.ToLower(strings.TrimSpace(value)) {
217+
case "1", "true", "yes", "on":
218+
return true
219+
default:
220+
return false
221+
}
222+
}
223+
200224
// calculateBinaryHash computes SHA256 hash of the binary
201225
func calculateBinaryHash(path string) (string, error) {
202226
file, err := os.Open(path)

app/plugins/plugin_loader_helper.go

Lines changed: 146 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,15 @@ func LoadPluginFunc(pluginDir, pluginID string) (func(map[string]interface{}) (i
4141
}
4242

4343
// Dynamic import based on plugin directory
44-
// The plugin must have a Plugin() function that returns a map with an "execute" key
44+
// Prefer executing the real source plugin via a generated wrapper program.
45+
// This avoids the old behavior where many plugins silently fell back to stubs
46+
// or incorrectly required a precompiled .so file.
4547
return func(params map[string]interface{}) (interface{}, error) {
46-
// Import the plugin using direct code execution
47-
//pluginName := filepath.Base(pluginDir)
48+
if result, err := executeSourcePlugin(pluginDir, pluginID, params); err == nil {
49+
return result, nil
50+
}
4851

49-
// Handle specific plugins based on their IDs
52+
// Fall back to legacy helper implementations only when direct execution fails.
5053
switch pluginID {
5154
case "subnet_calculator":
5255
// Use the ExecuteAdapter function from the subnet_calculator package
@@ -132,50 +135,160 @@ func executeCommand(command string) (string, error) {
132135
return output, err
133136
}
134137

135-
// Specific implementations for each plugin
136-
// These functions would typically be replaced by properly loading the plugin modules
137-
// but for now, we'll implement them with direct imports or simple placeholder functionality
138+
func executeSourcePlugin(pluginDir, pluginID string, params map[string]interface{}) (interface{}, error) {
139+
var err error
140+
pluginDir, err = filepath.Abs(pluginDir)
141+
if err != nil {
142+
return nil, fmt.Errorf("resolve absolute plugin dir for %s: %w", pluginID, err)
143+
}
138144

139-
func executeSubnetCalculator(params map[string]interface{}) (interface{}, error) {
140-
// Try to use a pre-compiled dynamic plugin (.so file)
141-
// NOTE: Do NOT check the registry here - this function can be called from
142-
// LoadPluginFunc which itself gets registered, causing infinite recursion
143-
pluginDir := filepath.Join("app", "plugins", "plugins", "subnet_calculator")
144-
pluginPath := filepath.Join(pluginDir, "subnet_calculator.so")
145+
pluginGoPath := filepath.Join(pluginDir, "plugin.go")
146+
content, err := os.ReadFile(pluginGoPath)
147+
if err != nil {
148+
return nil, fmt.Errorf("read plugin source for %s: %w", pluginID, err)
149+
}
150+
151+
paramsJSON, err := json.Marshal(params)
152+
if err != nil {
153+
return nil, fmt.Errorf("marshal parameters for %s: %w", pluginID, err)
154+
}
145155

146-
// Check if pre-compiled plugin exists
147-
if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
148-
return nil, fmt.Errorf("subnet_calculator plugin not available: no pre-compiled .so file")
156+
if strings.Contains(string(content), "package main") {
157+
cmd := exec.Command("go", "run", "plugin.go", "--execute="+string(paramsJSON))
158+
cmd.Dir = pluginDir
159+
output, err := cmd.CombinedOutput()
160+
if err != nil {
161+
return nil, fmt.Errorf("execute main plugin %s: %w: %s", pluginID, err, strings.TrimSpace(string(output)))
162+
}
163+
return decodePluginOutput(output)
149164
}
150165

151-
// Try to load the pre-compiled plugin
152-
p, err := plugin.Open(pluginPath)
166+
pluginModulePath, moduleRoot, err := resolvePluginModule(pluginDir)
153167
if err != nil {
154-
return nil, fmt.Errorf("failed to load subnet_calculator plugin: %v", err)
168+
return nil, err
155169
}
156170

157-
// Look up the Plugin symbol
158-
pluginSymbol, err := p.Lookup("Plugin")
171+
wrapperDir, err := os.MkdirTemp(moduleRoot, ".nettool-plugin-wrapper-*")
159172
if err != nil {
160-
return nil, fmt.Errorf("subnet_calculator plugin does not export Plugin symbol: %v", err)
173+
return nil, fmt.Errorf("create temp wrapper dir: %w", err)
161174
}
175+
defer os.RemoveAll(wrapperDir)
162176

163-
// Call the Plugin function
164-
pluginFunc := reflect.ValueOf(pluginSymbol).Call(nil)[0].Interface()
177+
importPath := pluginModulePath
178+
if pluginModulePath == "" {
179+
return nil, fmt.Errorf("empty module path for plugin %s", pluginID)
180+
}
181+
if pluginModulePath == "github.com/NetScout-Go/NetTool" {
182+
importPath = fmt.Sprintf("%s/app/plugins/plugins/%s", pluginModulePath, pluginID)
183+
}
184+
wrapperSource := fmt.Sprintf(`package main
185+
import (
186+
"encoding/json"
187+
"fmt"
188+
"os"
189+
pluginpkg %q
190+
)
191+
func main() {
192+
var params map[string]interface{}
193+
if err := json.Unmarshal([]byte(os.Args[1]), &params); err != nil {
194+
fmt.Fprintf(os.Stderr, "parse params: %%v", err)
195+
os.Exit(1)
196+
}
197+
result, err := pluginpkg.Execute(params)
198+
if err != nil {
199+
fmt.Fprintf(os.Stderr, "execute plugin: %%v", err)
200+
os.Exit(1)
201+
}
202+
if err := json.NewEncoder(os.Stdout).Encode(result); err != nil {
203+
fmt.Fprintf(os.Stderr, "encode result: %%v", err)
204+
os.Exit(1)
205+
}
206+
}
207+
`, importPath)
165208

166-
// Extract the execute function
167-
pluginMap, ok := pluginFunc.(map[string]interface{})
168-
if !ok {
169-
return nil, fmt.Errorf("subnet_calculator Plugin() did not return a map")
209+
mainPath := filepath.Join(wrapperDir, "main.go")
210+
if err := os.WriteFile(mainPath, []byte(wrapperSource), 0600); err != nil {
211+
return nil, fmt.Errorf("write wrapper source: %w", err)
170212
}
171213

172-
execFunc, ok := pluginMap["execute"].(func(map[string]interface{}) (interface{}, error))
173-
if !ok {
174-
return nil, fmt.Errorf("subnet_calculator does not provide a valid execute function")
214+
cmd := exec.Command("go", "run", mainPath, string(paramsJSON))
215+
cmd.Dir = moduleRoot
216+
output, err := cmd.CombinedOutput()
217+
if err != nil {
218+
return nil, fmt.Errorf("execute library plugin %s: %w: %s", pluginID, err, strings.TrimSpace(string(output)))
175219
}
176220

177-
// Call the execute function with the provided parameters
178-
return execFunc(params)
221+
return decodePluginOutput(output)
222+
}
223+
224+
func decodePluginOutput(output []byte) (interface{}, error) {
225+
trimmed := bytes.TrimSpace(output)
226+
var result interface{}
227+
if err := json.Unmarshal(trimmed, &result); err != nil {
228+
return map[string]interface{}{"result": string(trimmed)}, nil
229+
}
230+
return result, nil
231+
}
232+
233+
func findRepoRoot(start string) (string, error) {
234+
dir := start
235+
for {
236+
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
237+
return dir, nil
238+
}
239+
parent := filepath.Dir(dir)
240+
if parent == dir {
241+
return "", fmt.Errorf("could not locate repository root from %s", start)
242+
}
243+
dir = parent
244+
}
245+
}
246+
247+
func resolvePluginModule(pluginDir string) (modulePath string, moduleRoot string, err error) {
248+
pluginGoMod := filepath.Join(pluginDir, "go.mod")
249+
if _, statErr := os.Stat(pluginGoMod); statErr == nil {
250+
modulePath, err = readModulePath(pluginGoMod)
251+
if err != nil {
252+
return "", "", err
253+
}
254+
return modulePath, pluginDir, nil
255+
}
256+
257+
repoRoot, err := findRepoRoot(pluginDir)
258+
if err != nil {
259+
return "", "", err
260+
}
261+
modulePath, err = readModulePath(filepath.Join(repoRoot, "go.mod"))
262+
if err != nil {
263+
return "", "", err
264+
}
265+
return modulePath, repoRoot, nil
266+
}
267+
268+
func readModulePath(goModPath string) (string, error) {
269+
data, err := os.ReadFile(goModPath)
270+
if err != nil {
271+
return "", fmt.Errorf("read go.mod: %w", err)
272+
}
273+
for _, line := range strings.Split(string(data), "\n") {
274+
line = strings.TrimSpace(line)
275+
if strings.HasPrefix(line, "module ") {
276+
return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil
277+
}
278+
}
279+
return "", fmt.Errorf("module path not found in %s", goModPath)
280+
}
281+
282+
// Specific implementations for each plugin
283+
// These functions would typically be replaced by properly loading the plugin modules
284+
// but for now, we'll implement them with direct imports or simple placeholder functionality
285+
286+
func executeSubnetCalculator(params map[string]interface{}) (interface{}, error) {
287+
pluginDir := filepath.Join("app", "plugins", "plugins", "subnet_calculator")
288+
if result, err := executeSourcePlugin(pluginDir, "subnet_calculator", params); err == nil {
289+
return result, nil
290+
}
291+
return nil, fmt.Errorf("subnet_calculator plugin not available: no pre-compiled .so file")
179292
}
180293

181294
func executeNetworkLatencyHeatmap(params map[string]interface{}) (interface{}, error) {

0 commit comments

Comments
 (0)