Refine vm ports output

Make banger vm ports single-target and collapse the old VM/WEB table shape into a simpler PROTO ENDPOINT PROCESS COMMAND view. Web listeners now surface directly as http or https, with clickable endpoints in the main endpoint column instead of a separate URL field.

Classify TCP listeners with HTTPS-first probing so TLS services are not mislabeled as plain HTTP just because they answer bad cleartext requests with an HTTP error, then dedupe rows by rendered PROTO+ENDPOINT so dual-stack binds like 0.0.0.0 and :: only show once.

Update the CLI/daemon regressions and README to match the new contract. Verified with GOCACHE=/tmp/banger-gocache go test ./..., make build, git diff --check, and ./banger vm ports --help.
This commit is contained in:
Thales Maciel 2026-03-19 18:21:04 -03:00
parent 5ad3b505dd
commit 3096de0a7f
No known key found for this signature in database
GPG key ID: 33112E6833C34679
6 changed files with 179 additions and 151 deletions

View file

@ -2,6 +2,7 @@ package daemon
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
@ -78,8 +79,11 @@ func buildVMPorts(vm model.VMRecord, listeners []vsockagent.PortListener) []api.
if port.Command == "" {
port.Command = port.Process
}
if port.Proto == "tcp" && probeHost != "" && endpointHost != "" && probeHTTPListener(probeHost, listener.Port) {
port.WebURL = "http://" + net.JoinHostPort(endpointHost, strconv.Itoa(listener.Port)) + "/"
if port.Proto == "tcp" && probeHost != "" && endpointHost != "" {
if scheme, ok := probeWebListener(probeHost, listener.Port); ok {
port.Proto = scheme
port.Endpoint = scheme + "://" + net.JoinHostPort(endpointHost, strconv.Itoa(listener.Port)) + "/"
}
}
ports = append(ports, port)
}
@ -90,31 +94,49 @@ func buildVMPorts(vm model.VMRecord, listeners []vsockagent.PortListener) []api.
if ports[i].Port != ports[j].Port {
return ports[i].Port < ports[j].Port
}
if ports[i].PID != ports[j].PID {
return ports[i].PID < ports[j].PID
}
if ports[i].Process != ports[j].Process {
return ports[i].Process < ports[j].Process
}
return ports[i].Command < ports[j].Command
if ports[i].Command != ports[j].Command {
return ports[i].Command < ports[j].Command
}
return ports[i].BindAddress < ports[j].BindAddress
})
return ports
return dedupeVMPorts(ports)
}
func probeHTTPListener(guestIP string, port int) bool {
func probeWebListener(guestIP string, port int) (string, bool) {
if probeHTTPScheme("https", guestIP, port) {
return "https", true
}
if probeHTTPScheme("http", guestIP, port) {
return "http", true
}
return "", false
}
func probeHTTPScheme(scheme, guestIP string, port int) bool {
if strings.TrimSpace(guestIP) == "" || port <= 0 {
return false
}
url := "http://" + net.JoinHostPort(strings.TrimSpace(guestIP), strconv.Itoa(port)) + "/"
url := scheme + "://" + net.JoinHostPort(strings.TrimSpace(guestIP), strconv.Itoa(port)) + "/"
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return false
}
transport := &http.Transport{Proxy: nil}
if scheme == "https" {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
client := &http.Client{
Timeout: httpProbeTimeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
Proxy: nil,
},
Transport: transport,
}
resp, err := client.Do(req)
if err != nil {
@ -124,3 +146,20 @@ func probeHTTPListener(guestIP string, port int) bool {
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1))
return resp.ProtoMajor >= 1
}
func dedupeVMPorts(ports []api.VMPort) []api.VMPort {
if len(ports) < 2 {
return ports
}
deduped := make([]api.VMPort, 0, len(ports))
seen := make(map[string]struct{}, len(ports))
for _, port := range ports {
key := port.Proto + "\x00" + port.Endpoint
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
deduped = append(deduped, port)
}
return deduped
}