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:
parent
5ad3b505dd
commit
3096de0a7f
6 changed files with 179 additions and 151 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"banger/internal/paths"
|
||||
"banger/internal/store"
|
||||
"banger/internal/vmdns"
|
||||
"banger/internal/vsockagent"
|
||||
)
|
||||
|
||||
func TestFindVMPrefixResolution(t *testing.T) {
|
||||
|
|
@ -428,7 +429,7 @@ func TestHealthVMReturnsFalseForStoppedVM(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPortsVMReturnsEnrichedPortsAndWebURL(t *testing.T) {
|
||||
func TestPortsVMReturnsEnrichedPortsAndWebSchemes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -448,6 +449,20 @@ func TestPortsVMReturnsEnrichedPortsAndWebURL(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("ResolveTCPAddr: %v", err)
|
||||
}
|
||||
tlsServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
tlsListener, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen tls: %v", err)
|
||||
}
|
||||
tlsServer.Listener = tlsListener
|
||||
tlsServer.StartTLS()
|
||||
t.Cleanup(tlsServer.Close)
|
||||
tlsAddr, err := net.ResolveTCPAddr("tcp", strings.TrimPrefix(tlsServer.URL, "https://"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveTCPAddr(tls): %v", err)
|
||||
}
|
||||
|
||||
vsockSock := filepath.Join(t.TempDir(), "fc.vsock")
|
||||
listener, err := net.Listen("unix", vsockSock)
|
||||
|
|
@ -496,7 +511,7 @@ func TestPortsVMReturnsEnrichedPortsAndWebURL(t *testing.T) {
|
|||
serverDone <- fmt.Errorf("unexpected ports payload %q", got)
|
||||
return
|
||||
}
|
||||
body := fmt.Sprintf(`{"listeners":[{"proto":"tcp","bind_address":"0.0.0.0","port":%d,"pid":44,"process":"python3","command":"python3 -m http.server %d"},{"proto":"udp","bind_address":"0.0.0.0","port":53,"pid":1,"process":"dnsd","command":"dnsd --foreground"}]}`, webAddr.Port, webAddr.Port)
|
||||
body := fmt.Sprintf(`{"listeners":[{"proto":"tcp","bind_address":"0.0.0.0","port":%d,"pid":44,"process":"python3","command":"python3 -m http.server %d"},{"proto":"tcp","bind_address":"0.0.0.0","port":%d,"pid":77,"process":"caddy","command":"caddy run"},{"proto":"udp","bind_address":"0.0.0.0","port":53,"pid":1,"process":"dnsd","command":"dnsd --foreground"}]}`, webAddr.Port, webAddr.Port, tlsAddr.Port)
|
||||
resp := fmt.Sprintf("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s", len(body), body)
|
||||
_, err = conn.Write([]byte(resp))
|
||||
serverDone <- err
|
||||
|
|
@ -527,24 +542,30 @@ func TestPortsVMReturnsEnrichedPortsAndWebURL(t *testing.T) {
|
|||
if result.Name != vm.Name || result.DNSName != vm.Runtime.DNSName {
|
||||
t.Fatalf("result = %+v, want name/dns", result)
|
||||
}
|
||||
if len(result.Ports) != 2 {
|
||||
t.Fatalf("ports = %+v, want 2 entries", result.Ports)
|
||||
if len(result.Ports) != 3 {
|
||||
t.Fatalf("ports = %+v, want 3 entries", result.Ports)
|
||||
}
|
||||
wantWeb := fmt.Sprintf("http://ports.vm:%d/", webAddr.Port)
|
||||
var tcpPort, udpPort api.VMPort
|
||||
wantHTTP := fmt.Sprintf("http://ports.vm:%d/", webAddr.Port)
|
||||
wantHTTPS := fmt.Sprintf("https://ports.vm:%d/", tlsAddr.Port)
|
||||
var httpPort, httpsPort, udpPort api.VMPort
|
||||
for _, port := range result.Ports {
|
||||
switch port.Proto {
|
||||
case "tcp":
|
||||
tcpPort = port
|
||||
case "udp":
|
||||
switch port.Port {
|
||||
case webAddr.Port:
|
||||
httpPort = port
|
||||
case tlsAddr.Port:
|
||||
httpsPort = port
|
||||
case 53:
|
||||
udpPort = port
|
||||
}
|
||||
}
|
||||
if udpPort.Endpoint != "ports.vm:53" || udpPort.WebURL != "" {
|
||||
if udpPort.Endpoint != "ports.vm:53" {
|
||||
t.Fatalf("udp port = %+v, want endpoint only", udpPort)
|
||||
}
|
||||
if tcpPort.Endpoint != net.JoinHostPort("ports.vm", strconv.Itoa(webAddr.Port)) || tcpPort.WebURL != wantWeb {
|
||||
t.Fatalf("tcp port = %+v, want web url %q", tcpPort, wantWeb)
|
||||
if httpPort.Proto != "http" || httpPort.Endpoint != wantHTTP {
|
||||
t.Fatalf("http port = %+v, want http endpoint %q", httpPort, wantHTTP)
|
||||
}
|
||||
if httpsPort.Proto != "https" || httpsPort.Endpoint != wantHTTPS {
|
||||
t.Fatalf("https port = %+v, want https endpoint %q", httpsPort, wantHTTPS)
|
||||
}
|
||||
runner.assertExhausted()
|
||||
if err := <-serverDone; err != nil {
|
||||
|
|
@ -567,6 +588,49 @@ func TestPortsVMReturnsErrorForStoppedVM(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuildVMPortsDeduplicatesSameRenderedEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
vm := testVM("dedupe-ports", "image-ports", "")
|
||||
vm.Runtime.DNSName = "dedupe-ports.vm"
|
||||
|
||||
ports := buildVMPorts(vm, []vsockagent.PortListener{
|
||||
{
|
||||
Proto: "tcp",
|
||||
BindAddress: "0.0.0.0",
|
||||
Port: 8080,
|
||||
PID: 44,
|
||||
Process: "docker-proxy",
|
||||
Command: "/usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080",
|
||||
},
|
||||
{
|
||||
Proto: "tcp",
|
||||
BindAddress: "::",
|
||||
Port: 8080,
|
||||
PID: 45,
|
||||
Process: "docker-proxy",
|
||||
Command: "/usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080",
|
||||
},
|
||||
{
|
||||
Proto: "udp",
|
||||
BindAddress: "0.0.0.0",
|
||||
Port: 8080,
|
||||
PID: 46,
|
||||
Process: "dnsd",
|
||||
Command: "dnsd --foreground",
|
||||
},
|
||||
})
|
||||
if len(ports) != 2 {
|
||||
t.Fatalf("ports = %+v, want tcp+udp entries after dedupe", ports)
|
||||
}
|
||||
if ports[0].Proto != "tcp" || ports[0].Endpoint != "dedupe-ports.vm:8080" {
|
||||
t.Fatalf("first port = %+v, want deduped tcp endpoint", ports[0])
|
||||
}
|
||||
if ports[1].Proto != "udp" || ports[1].Endpoint != "dedupe-ports.vm:8080" {
|
||||
t.Fatalf("second port = %+v, want distinct udp endpoint", ports[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetVMDiskResizeFailsPreflightWhenToolsMissing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue