mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
feat(support): collect system info bundle to assist support troubleshooting [r8s-157] (#154)
This commit is contained in:
parent
17648d12fe
commit
783ab253af
17 changed files with 1367 additions and 440 deletions
136
pkg/networking/diagnostics.go
Normal file
136
pkg/networking/diagnostics.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package networking
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
// ProbeDNSConnection probes a DNS connection and returns a JSON string with the DNS lookup status and IP addresses.
|
||||
// ignores errors for the dns lookup since we want to know if the host is reachable
|
||||
func ProbeDNSConnection(url string) string {
|
||||
_, host, _ := parseURL(url)
|
||||
result := map[string]interface{}{
|
||||
"operation": "dns lookup",
|
||||
"remote_address": host,
|
||||
"connected_at": time.Now().Format(time.RFC3339),
|
||||
"status": "dns lookup successful",
|
||||
"resolved_ips": []net.IP{},
|
||||
}
|
||||
|
||||
ipAddresses, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
result["status"] = fmt.Sprintf("dns lookup failed: %s", err)
|
||||
} else {
|
||||
result["resolved_ips"] = ipAddresses
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(result)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
// ProbeTelnetConnection probes a telnet connection and returns a JSON string with the telnet connection status, local and remote addresses.
|
||||
// ignores errors for the telnet connection since we want to know if the host is reachable
|
||||
func ProbeTelnetConnection(url string) string {
|
||||
network, host, port := parseURL(url)
|
||||
if network == "https" || network == "http" {
|
||||
network = "tcp"
|
||||
}
|
||||
|
||||
address := fmt.Sprintf("%s:%s", host, port)
|
||||
result := map[string]string{
|
||||
"operation": "telnet connection",
|
||||
"local_address": "unknown",
|
||||
"remote_address": "unknown",
|
||||
"network": network,
|
||||
"status": "connected to " + address,
|
||||
"connected_at": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
connection, err := net.DialTimeout(network, address, 5*time.Second)
|
||||
if err != nil {
|
||||
result["status"] = fmt.Sprintf("failed to connect to %s: %s", address, err)
|
||||
} else {
|
||||
defer connection.Close()
|
||||
result["local_address"] = connection.LocalAddr().String()
|
||||
result["remote_address"] = connection.RemoteAddr().String()
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(result)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
// DetectProxy probes a target URL and returns a JSON string with the proxy detection status, local and remote addresses.
|
||||
// ignores errors for the http request since we want to know if the host is reachable
|
||||
func DetectProxy(url string) string {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
result := map[string]string{
|
||||
"operation": "proxy detection",
|
||||
"local_address": "unknown",
|
||||
"remote_address": "unknown",
|
||||
"network": "https",
|
||||
"status": "no proxy detected",
|
||||
"connected_at": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
result["status"] = fmt.Sprintf("failed to make request: %s", err)
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.Request != nil {
|
||||
result["local_address"] = resp.Request.Host
|
||||
result["remote_address"] = resp.Request.RemoteAddr
|
||||
}
|
||||
|
||||
if resp.Header.Get("Via") != "" || resp.Header.Get("X-Forwarded-For") != "" || resp.Header.Get("Proxy-Connection") != "" {
|
||||
result["status"] = "proxy detected via headers"
|
||||
} else if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
|
||||
cert := resp.TLS.PeerCertificates[0]
|
||||
if cert.IsCA || strings.Contains(strings.ToLower(cert.Issuer.CommonName), "proxy") {
|
||||
result["status"] = "proxy detected via certificate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(result)
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
// parseURL parses a raw URL and returns the network, host and port
|
||||
// it also ensures the network is tcp and the port is set to the default for the network
|
||||
func parseURL(rawURL string) (network, host, port string) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
network = u.Scheme
|
||||
host = u.Hostname()
|
||||
port = u.Port()
|
||||
|
||||
if port == "" {
|
||||
if network == "https" {
|
||||
port = "443"
|
||||
} else if network == "http" {
|
||||
port = "80"
|
||||
}
|
||||
}
|
||||
|
||||
return network, host, port
|
||||
}
|
164
pkg/networking/diagnostics_test.go
Normal file
164
pkg/networking/diagnostics_test.go
Normal file
|
@ -0,0 +1,164 @@
|
|||
package networking
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Response structs for each function
|
||||
type dnsResponse struct {
|
||||
Operation string `json:"operation"`
|
||||
ResolvedIPs []string `json:"resolved_ips"`
|
||||
RemoteAddr string `json:"remote_address"`
|
||||
ConnectedAt string `json:"connected_at"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type telnetResponse struct {
|
||||
Operation string `json:"operation"`
|
||||
LocalAddr string `json:"local_address"`
|
||||
RemoteAddr string `json:"remote_address"`
|
||||
Network string `json:"network"`
|
||||
Status string `json:"status"`
|
||||
ConnectedAt string `json:"connected_at"`
|
||||
}
|
||||
|
||||
type proxyResponse struct {
|
||||
Operation string `json:"operation"`
|
||||
LocalAddr string `json:"local_address"`
|
||||
RemoteAddr string `json:"remote_address"`
|
||||
Network string `json:"network"`
|
||||
Status string `json:"status"`
|
||||
ConnectedAt string `json:"connected_at"`
|
||||
}
|
||||
|
||||
func TestProbeDNSConnection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
wantSuccess bool
|
||||
statusContains string
|
||||
}{
|
||||
{
|
||||
name: "Valid domain",
|
||||
host: "https://api.portainer.io",
|
||||
wantSuccess: true,
|
||||
statusContains: "dns lookup successful",
|
||||
},
|
||||
{
|
||||
name: "Invalid domain",
|
||||
host: "https://nonexistent.domain.invalid",
|
||||
wantSuccess: false,
|
||||
statusContains: "dns lookup failed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
response := ProbeDNSConnection(tt.host)
|
||||
|
||||
var result dnsResponse
|
||||
if err := json.Unmarshal([]byte(response), &result); err != nil {
|
||||
t.Fatalf("Invalid JSON response: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(result.Status, tt.statusContains) {
|
||||
t.Errorf("Status should contain '%s', got: %s", tt.statusContains, result.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeTelnetConnection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantSuccess bool
|
||||
statusContains string
|
||||
}{
|
||||
{
|
||||
name: "Valid connection",
|
||||
url: "https://api.portainer.io",
|
||||
wantSuccess: true,
|
||||
statusContains: "connected to",
|
||||
},
|
||||
{
|
||||
name: "Invalid port",
|
||||
url: "https://api.portainer.io:99999",
|
||||
wantSuccess: false,
|
||||
statusContains: "failed to connect",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
response := ProbeTelnetConnection(tt.url)
|
||||
|
||||
var result telnetResponse
|
||||
if err := json.Unmarshal([]byte(response), &result); err != nil {
|
||||
t.Fatalf("Invalid JSON response: %v", err)
|
||||
}
|
||||
|
||||
validateCommonFields(t, result.Operation, "telnet connection", result.ConnectedAt)
|
||||
if !strings.Contains(result.Status, tt.statusContains) {
|
||||
t.Errorf("Status should contain '%s', got: %s", tt.statusContains, result.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantSuccess bool
|
||||
statusContains string
|
||||
}{
|
||||
{
|
||||
name: "Valid URL",
|
||||
url: "https://api.portainer.io",
|
||||
wantSuccess: true,
|
||||
statusContains: "proxy",
|
||||
},
|
||||
{
|
||||
name: "Invalid URL",
|
||||
url: "https://nonexistent.domain.invalid",
|
||||
wantSuccess: false,
|
||||
statusContains: "failed to make request",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
response := DetectProxy(tt.url)
|
||||
|
||||
var result proxyResponse
|
||||
if err := json.Unmarshal([]byte(response), &result); err != nil {
|
||||
t.Fatalf("Invalid JSON response: %v", err)
|
||||
}
|
||||
|
||||
validateCommonFields(t, result.Operation, "proxy detection", result.ConnectedAt)
|
||||
if result.Network != "https" {
|
||||
t.Errorf("Expected network https, got %s", result.Network)
|
||||
}
|
||||
if !strings.Contains(result.Status, tt.statusContains) {
|
||||
t.Errorf("Status should contain '%s', got: %s", tt.statusContains, result.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to validate common fields across all responses
|
||||
func validateCommonFields(t *testing.T, operation, expectedOperation, connectedAt string) {
|
||||
t.Helper()
|
||||
|
||||
if operation != expectedOperation {
|
||||
t.Errorf("Expected operation '%s', got '%s'", expectedOperation, operation)
|
||||
}
|
||||
|
||||
if _, err := time.Parse(time.RFC3339, connectedAt); err != nil {
|
||||
t.Errorf("Invalid connected_at timestamp: %v", err)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue