mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-31 01:58:15 -04:00
Add -4/-6 IP version flags to proxy debug ping, WASM ping, and SSH
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
@@ -166,39 +167,58 @@ func createSSHMethod(client *netbird.Client) js.Func {
|
||||
})
|
||||
}
|
||||
|
||||
var jwtToken string
|
||||
if len(args) > 3 && !args[3].IsNull() && !args[3].IsUndefined() {
|
||||
jwtToken = args[3].String()
|
||||
}
|
||||
jwtToken, ipVersion := parseSSHOptions(args)
|
||||
|
||||
return createPromise(func(resolve, reject js.Value) {
|
||||
sshClient := ssh.NewClient(client)
|
||||
|
||||
if err := sshClient.Connect(host, port, username, jwtToken); err != nil {
|
||||
jsInterface, err := connectSSH(client, host, port, username, jwtToken, ipVersion)
|
||||
if err != nil {
|
||||
reject.Invoke(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := sshClient.StartSession(80, 24); err != nil {
|
||||
if closeErr := sshClient.Close(); closeErr != nil {
|
||||
log.Errorf("Error closing SSH client: %v", closeErr)
|
||||
}
|
||||
reject.Invoke(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
jsInterface := ssh.CreateJSInterface(sshClient)
|
||||
resolve.Invoke(jsInterface)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func performPing(client *netbird.Client, hostname string) {
|
||||
func parseSSHOptions(args []js.Value) (jwtToken string, ipVersion int) {
|
||||
if len(args) > 3 && !args[3].IsNull() && !args[3].IsUndefined() {
|
||||
jwtToken = args[3].String()
|
||||
}
|
||||
if len(args) > 4 {
|
||||
ipVersion = jsIPVersion(args[4])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func connectSSH(client *netbird.Client, host string, port int, username, jwtToken string, ipVersion int) (js.Value, error) {
|
||||
sshClient := ssh.NewClient(client)
|
||||
|
||||
if err := sshClient.Connect(host, port, username, jwtToken, ipVersion); err != nil {
|
||||
return js.Undefined(), err
|
||||
}
|
||||
|
||||
if err := sshClient.StartSession(80, 24); err != nil {
|
||||
if closeErr := sshClient.Close(); closeErr != nil {
|
||||
log.Errorf("Error closing SSH client: %v", closeErr)
|
||||
}
|
||||
return js.Undefined(), err
|
||||
}
|
||||
|
||||
return ssh.CreateJSInterface(sshClient), nil
|
||||
}
|
||||
|
||||
func performPing(client *netbird.Client, hostname string, ipVersion int) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), pingTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Default to ping4 to avoid dual-stack ICMP endpoint issues in wireguard-go netstack.
|
||||
network := "ping4"
|
||||
if ipVersion == 6 {
|
||||
network = "ping6"
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
conn, err := client.Dial(ctx, "ping", hostname)
|
||||
conn, err := client.Dial(ctx, network, hostname)
|
||||
if err != nil {
|
||||
js.Global().Get("console").Call("log", fmt.Sprintf("Ping to %s failed: %v", hostname, err))
|
||||
return
|
||||
@@ -225,27 +245,39 @@ func performPing(client *netbird.Client, hostname string) {
|
||||
}
|
||||
|
||||
latency := time.Since(start)
|
||||
js.Global().Get("console").Call("log", fmt.Sprintf("Ping to %s: %dms", hostname, latency.Milliseconds()))
|
||||
remote := conn.RemoteAddr().String()
|
||||
msg := fmt.Sprintf("Ping to %s: %dms", hostname, latency.Milliseconds())
|
||||
if remote != hostname {
|
||||
msg += fmt.Sprintf(" (via %s)", remote)
|
||||
}
|
||||
js.Global().Get("console").Call("log", msg)
|
||||
}
|
||||
|
||||
func performPingTCP(client *netbird.Client, hostname string, port int) {
|
||||
func performPingTCP(client *netbird.Client, hostname string, port, ipVersion int) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), pingTimeout)
|
||||
defer cancel()
|
||||
|
||||
network := ipVersionNetwork("tcp", ipVersion)
|
||||
|
||||
address := net.JoinHostPort(hostname, fmt.Sprintf("%d", port))
|
||||
start := time.Now()
|
||||
conn, err := client.Dial(ctx, "tcp", address)
|
||||
conn, err := client.Dial(ctx, network, address)
|
||||
if err != nil {
|
||||
js.Global().Get("console").Call("log", fmt.Sprintf("TCP ping to %s failed: %v", address, err))
|
||||
return
|
||||
}
|
||||
latency := time.Since(start)
|
||||
|
||||
remote := conn.RemoteAddr().String()
|
||||
if err := conn.Close(); err != nil {
|
||||
log.Debugf("failed to close TCP connection: %v", err)
|
||||
}
|
||||
|
||||
js.Global().Get("console").Call("log", fmt.Sprintf("TCP ping to %s succeeded: %dms", address, latency.Milliseconds()))
|
||||
msg := fmt.Sprintf("TCP ping to %s succeeded: %dms", address, latency.Milliseconds())
|
||||
if remote != address {
|
||||
msg += fmt.Sprintf(" (via %s)", remote)
|
||||
}
|
||||
js.Global().Get("console").Call("log", msg)
|
||||
}
|
||||
|
||||
// createPingMethod creates the ping method
|
||||
@@ -262,8 +294,12 @@ func createPingMethod(client *netbird.Client) js.Func {
|
||||
}
|
||||
|
||||
hostname := args[0].String()
|
||||
var ipVersion int
|
||||
if len(args) > 1 {
|
||||
ipVersion = jsIPVersion(args[1])
|
||||
}
|
||||
return createPromise(func(resolve, reject js.Value) {
|
||||
performPing(client, hostname)
|
||||
performPing(client, hostname, ipVersion)
|
||||
resolve.Invoke(js.Undefined())
|
||||
})
|
||||
})
|
||||
@@ -290,8 +326,12 @@ func createPingTCPMethod(client *netbird.Client) js.Func {
|
||||
|
||||
hostname := args[0].String()
|
||||
port := args[1].Int()
|
||||
var ipVersion int
|
||||
if len(args) > 2 {
|
||||
ipVersion = jsIPVersion(args[2])
|
||||
}
|
||||
return createPromise(func(resolve, reject js.Value) {
|
||||
performPingTCP(client, hostname, port)
|
||||
performPingTCP(client, hostname, port, ipVersion)
|
||||
resolve.Invoke(js.Undefined())
|
||||
})
|
||||
})
|
||||
@@ -464,6 +504,31 @@ func createSetLogLevelMethod(client *netbird.Client) js.Func {
|
||||
})
|
||||
}
|
||||
|
||||
// ipVersionNetwork appends "4" or "6" to a base network string (e.g. "tcp" -> "tcp4").
|
||||
func ipVersionNetwork(base string, ipVersion int) string {
|
||||
switch ipVersion {
|
||||
case 4:
|
||||
return base + "4"
|
||||
case 6:
|
||||
return base + "6"
|
||||
default:
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
// jsIPVersion extracts an IP version (4 or 6) from a JS string or number.
|
||||
func jsIPVersion(v js.Value) int {
|
||||
switch v.Type() {
|
||||
case js.TypeNumber:
|
||||
return v.Int()
|
||||
case js.TypeString:
|
||||
n, _ := strconv.Atoi(v.String())
|
||||
return n
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// createPromise is a helper to create JavaScript promises
|
||||
func createPromise(handler func(resolve, reject js.Value)) js.Value {
|
||||
return js.Global().Get("Promise").New(js.FuncOf(func(_ js.Value, promiseArgs []js.Value) any {
|
||||
|
||||
@@ -46,8 +46,9 @@ func NewClient(nbClient *netbird.Client) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes an SSH connection through NetBird network
|
||||
func (c *Client) Connect(host string, port int, username, jwtToken string) error {
|
||||
// Connect establishes an SSH connection through NetBird network.
|
||||
// ipVersion may be 4, 6, or 0 for automatic selection.
|
||||
func (c *Client) Connect(host string, port int, username, jwtToken string, ipVersion int) error {
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
logrus.Infof("SSH: Connecting to %s as %s", addr, username)
|
||||
|
||||
@@ -63,10 +64,18 @@ func (c *Client) Connect(host string, port int, username, jwtToken string) error
|
||||
Timeout: sshDialTimeout,
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
switch ipVersion {
|
||||
case 4:
|
||||
network = "tcp4"
|
||||
case 6:
|
||||
network = "tcp6"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), sshDialTimeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := c.nbClient.Dial(ctx, "tcp", addr)
|
||||
conn, err := c.nbClient.Dial(ctx, network, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial %s: %w", addr, err)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -57,7 +58,11 @@ var debugSyncCmd = &cobra.Command{
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var pingTimeout string
|
||||
var (
|
||||
pingTimeout time.Duration
|
||||
pingIPv4 bool
|
||||
pingIPv6 bool
|
||||
)
|
||||
|
||||
var debugPingCmd = &cobra.Command{
|
||||
Use: "ping <account-id> <host> [port]",
|
||||
@@ -108,7 +113,10 @@ func init() {
|
||||
debugStatusCmd.Flags().StringVar(&statusFilterByStatus, "filter-by-status", "", "Filter by status (idle|connecting|connected)")
|
||||
debugStatusCmd.Flags().StringVar(&statusFilterByConnectionType, "filter-by-connection-type", "", "Filter by connection type (P2P|Relayed)")
|
||||
|
||||
debugPingCmd.Flags().StringVar(&pingTimeout, "timeout", "", "Ping timeout (e.g., 10s)")
|
||||
debugPingCmd.Flags().DurationVar(&pingTimeout, "timeout", 0, "Ping timeout (e.g., 10s)")
|
||||
debugPingCmd.Flags().BoolVarP(&pingIPv4, "ipv4", "4", false, "Force IPv4")
|
||||
debugPingCmd.Flags().BoolVarP(&pingIPv6, "ipv6", "6", false, "Force IPv6")
|
||||
debugPingCmd.MarkFlagsMutuallyExclusive("ipv4", "ipv6")
|
||||
|
||||
debugCmd.AddCommand(debugHealthCmd)
|
||||
debugCmd.AddCommand(debugClientsCmd)
|
||||
@@ -157,7 +165,14 @@ func runDebugPing(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
port = p
|
||||
}
|
||||
return getDebugClient(cmd).PingTCP(cmd.Context(), args[0], args[1], port, pingTimeout)
|
||||
var ipVersion string
|
||||
switch {
|
||||
case pingIPv4:
|
||||
ipVersion = "4"
|
||||
case pingIPv6:
|
||||
ipVersion = "6"
|
||||
}
|
||||
return getDebugClient(cmd).PingTCP(cmd.Context(), args[0], args[1], port, pingTimeout, ipVersion)
|
||||
}
|
||||
|
||||
func runDebugLogLevel(cmd *cobra.Command, args []string) error {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
)
|
||||
|
||||
// StatusFilters contains filter options for status queries.
|
||||
@@ -230,12 +231,16 @@ func (c *Client) ClientSyncResponse(ctx context.Context, accountID string) error
|
||||
}
|
||||
|
||||
// PingTCP performs a TCP ping through a client.
|
||||
func (c *Client) PingTCP(ctx context.Context, accountID, host string, port int, timeout string) error {
|
||||
// ipVersion may be "4", "6", or "" for automatic.
|
||||
func (c *Client) PingTCP(ctx context.Context, accountID, host string, port int, timeout time.Duration, ipVersion string) error {
|
||||
params := url.Values{}
|
||||
params.Set("host", host)
|
||||
params.Set("port", fmt.Sprintf("%d", port))
|
||||
if timeout != "" {
|
||||
params.Set("timeout", timeout)
|
||||
if timeout > 0 {
|
||||
params.Set("timeout", timeout.String())
|
||||
}
|
||||
if ipVersion != "" {
|
||||
params.Set("ip_version", ipVersion)
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/debug/clients/%s/pingtcp?%s", url.PathEscape(accountID), params.Encode())
|
||||
@@ -245,7 +250,13 @@ func (c *Client) PingTCP(ctx context.Context, accountID, host string, port int,
|
||||
func (c *Client) printPingResult(data map[string]any) {
|
||||
success, _ := data["success"].(bool)
|
||||
if success {
|
||||
_, _ = fmt.Fprintf(c.out, "Success: %v:%v\n", data["host"], data["port"])
|
||||
remote, _ := data["remote"].(string)
|
||||
host := fmt.Sprintf("%v:%v", data["host"], data["port"])
|
||||
if remote != "" && remote != host {
|
||||
_, _ = fmt.Fprintf(c.out, "Success: %s (via %s)\n", host, remote)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(c.out, "Success: %s\n", host)
|
||||
}
|
||||
_, _ = fmt.Fprintf(c.out, "Latency: %v\n", data["latency"])
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(c.out, "Failed: %v:%v\n", data["host"], data["port"])
|
||||
|
||||
@@ -526,13 +526,18 @@ func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountI
|
||||
}
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
if v := r.URL.Query().Get("ip_version"); v == "4" || v == "6" {
|
||||
network += v
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), timeout)
|
||||
defer cancel()
|
||||
|
||||
address := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
start := time.Now()
|
||||
|
||||
conn, err := client.Dial(ctx, "tcp", address)
|
||||
conn, err := client.Dial(ctx, network, address)
|
||||
if err != nil {
|
||||
h.writeJSON(w, map[string]interface{}{
|
||||
"success": false,
|
||||
@@ -542,18 +547,22 @@ func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountI
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
remote := conn.RemoteAddr().String()
|
||||
if err := conn.Close(); err != nil {
|
||||
h.logger.Debugf("close tcp ping connection: %v", err)
|
||||
}
|
||||
|
||||
latency := time.Since(start)
|
||||
h.writeJSON(w, map[string]interface{}{
|
||||
resp := map[string]interface{}{
|
||||
"success": true,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"remote": remote,
|
||||
"latency_ms": latency.Milliseconds(),
|
||||
"latency": formatDuration(latency),
|
||||
})
|
||||
}
|
||||
h.writeJSON(w, resp)
|
||||
}
|
||||
|
||||
func (h *Handler) handleLogLevel(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
|
||||
|
||||
Reference in New Issue
Block a user