mirror of https://github.com/fatedier/frp
Interface Binding Support for frpc
parent
024c334d9d
commit
07c8bb74ec
35
README.md
35
README.md
|
@ -1207,6 +1207,41 @@ serverPort = 7000
|
|||
transport.proxyURL = "http://user:pwd@192.168.1.128:8080"
|
||||
```
|
||||
|
||||
### Network Interface Binding
|
||||
|
||||
frpc can bind to specific network interfaces when connecting to frps, which is useful for multi-network systems or when you need to route traffic through specific network paths.
|
||||
|
||||
```toml
|
||||
# frpc.toml
|
||||
serverAddr = "x.x.x.x"
|
||||
serverPort = 7000
|
||||
|
||||
[transport]
|
||||
# Bind to a specific network interface
|
||||
connectServerInterface = "eth0"
|
||||
|
||||
# Or use auto-detection for first available interface
|
||||
# connectServerInterface = "auto"
|
||||
|
||||
# Or bind to a specific IP address (existing feature)
|
||||
# connectServerLocalIP = "192.168.1.100"
|
||||
```
|
||||
|
||||
You can also specify interface binding via command line:
|
||||
|
||||
```bash
|
||||
# Bind to specific interface
|
||||
./frpc --bind-interface eth0 -c frpc.toml
|
||||
|
||||
# Auto-detect interface
|
||||
./frpc --bind-interface auto -c frpc.toml
|
||||
|
||||
# Bind to specific IP
|
||||
./frpc --bind-ip 192.168.1.100 -c frpc.toml
|
||||
```
|
||||
|
||||
This feature supports all protocols except kcp and works across Linux, macOS, and Windows platforms.
|
||||
|
||||
### Port range mapping
|
||||
|
||||
*Added in v0.56.0*
|
||||
|
|
|
@ -196,8 +196,14 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
|
|||
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
|
||||
}
|
||||
|
||||
if c.cfg.Transport.ConnectServerLocalIP != "" {
|
||||
dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
|
||||
// Resolve binding address from interface name or IP address
|
||||
bindAddr, err := c.resolveBindAddress()
|
||||
if err != nil {
|
||||
xl.Errorf("failed to resolve bind address: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if bindAddr != "" {
|
||||
dialOptions = append(dialOptions, libnet.WithLocalAddr(bindAddr))
|
||||
}
|
||||
dialOptions = append(dialOptions,
|
||||
libnet.WithProtocol(protocol),
|
||||
|
@ -214,6 +220,20 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
|
|||
return conn, err
|
||||
}
|
||||
|
||||
// resolveBindAddress resolves the binding address from interface name or IP address
|
||||
func (c *defaultConnectorImpl) resolveBindAddress() (string, error) {
|
||||
// Priority: Interface name > IP address
|
||||
if c.cfg.Transport.ConnectServerInterface != "" {
|
||||
return netpkg.ResolveBindAddress(c.cfg.Transport.ConnectServerInterface)
|
||||
}
|
||||
|
||||
if c.cfg.Transport.ConnectServerLocalIP != "" {
|
||||
return c.cfg.Transport.ConnectServerLocalIP, nil
|
||||
}
|
||||
|
||||
return "", nil // No binding specified
|
||||
}
|
||||
|
||||
func (c *defaultConnectorImpl) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
if c.quicConn != nil {
|
||||
|
|
|
@ -88,9 +88,16 @@ transport.poolCount = 5
|
|||
transport.protocol = "tcp"
|
||||
|
||||
# set client binding ip when connect server, default is empty.
|
||||
# only when protocol = tcp or websocket, the value will be used.
|
||||
# works with tcp, websocket, http, https, and quic protocols. Not supported with kcp protocol.
|
||||
transport.connectServerLocalIP = "0.0.0.0"
|
||||
|
||||
# set client binding interface when connect server, default is empty.
|
||||
# valid values are interface names (e.g., "eth0", "wlan0") or "auto" for auto-detection.
|
||||
# works with tcp, websocket, http, https, and quic protocols. Not supported with kcp protocol.
|
||||
# priority: if both connectServerLocalIP and connectServerInterface are set, connectServerInterface takes precedence.
|
||||
# transport.connectServerInterface = "eth0"
|
||||
# transport.connectServerInterface = "auto"
|
||||
|
||||
# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set proxyURL here or in global environment variables
|
||||
# it only works when protocol is tcp
|
||||
# transport.proxyURL = "http://user:passwd@192.168.1.128:8080"
|
||||
|
|
|
@ -99,9 +99,16 @@ login_fail_exit = true
|
|||
protocol = tcp
|
||||
|
||||
# set client binding ip when connect server, default is empty.
|
||||
# only when protocol = tcp or websocket, the value will be used.
|
||||
# works with tcp, websocket, http, https, and quic protocols. Not supported with kcp protocol.
|
||||
connect_server_local_ip = 0.0.0.0
|
||||
|
||||
# set client binding interface when connect server, default is empty.
|
||||
# valid values are interface names (e.g., "eth0", "wlan0") or "auto" for auto-detection.
|
||||
# works with tcp, websocket, http, https, and quic protocols. Not supported with kcp protocol.
|
||||
# priority: if both connect_server_local_ip and connect_server_interface are set, connect_server_interface takes precedence.
|
||||
# connect_server_interface = eth0
|
||||
# connect_server_interface = auto
|
||||
|
||||
# quic protocol options
|
||||
# quic_keepalive_period = 10
|
||||
# quic_max_idle_timeout = 30
|
||||
|
|
|
@ -164,6 +164,8 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
|
|||
cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console")
|
||||
cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate")
|
||||
cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one")
|
||||
cmd.PersistentFlags().StringVarP(&c.Transport.ConnectServerInterface, "bind-interface", "", "", "network interface to bind when connecting to server (e.g., eth0, wlan0, auto)")
|
||||
cmd.PersistentFlags().StringVarP(&c.Transport.ConnectServerLocalIP, "bind-ip", "", "", "IP address to bind when connecting to server")
|
||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||
}
|
||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||
|
|
|
@ -49,8 +49,14 @@ type ClientCommonConf struct {
|
|||
DialServerKeepAlive int64 `ini:"dial_server_keepalive" json:"dial_server_keepalive"`
|
||||
// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
|
||||
// By default, this value is empty.
|
||||
// this value only use in TCP/Websocket protocol. Not support in KCP protocol.
|
||||
// this value works with tcp, websocket, http, https, and quic protocols. Not supported with kcp protocol.
|
||||
ConnectServerLocalIP string `ini:"connect_server_local_ip" json:"connect_server_local_ip"`
|
||||
// ConnectServerInterface specifies the network interface name to bind when connecting to server.
|
||||
// Valid values are interface names (e.g., "eth0", "wlan0") or "auto" for auto-detection.
|
||||
// By default, this value is empty.
|
||||
// this value works with tcp, websocket, http, https, and quic protocols. Not supported with kcp protocol.
|
||||
// Priority: If both connect_server_local_ip and connect_server_interface are set, connect_server_interface takes precedence.
|
||||
ConnectServerInterface string `ini:"connect_server_interface" json:"connect_server_interface"`
|
||||
// HTTPProxy specifies a proxy address to connect to the server through. If
|
||||
// this value is "", the server will be connected to directly. By default,
|
||||
// this value is read from the "http_proxy" environment variable.
|
||||
|
|
|
@ -47,6 +47,7 @@ func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConf
|
|||
out.Transport.DialServerTimeout = conf.DialServerTimeout
|
||||
out.Transport.DialServerKeepAlive = conf.DialServerKeepAlive
|
||||
out.Transport.ConnectServerLocalIP = conf.ConnectServerLocalIP
|
||||
out.Transport.ConnectServerInterface = conf.ConnectServerInterface
|
||||
out.Transport.ProxyURL = conf.HTTPProxy
|
||||
out.Transport.PoolCount = conf.PoolCount
|
||||
out.Transport.TCPMux = lo.ToPtr(conf.TCPMux)
|
||||
|
|
|
@ -107,8 +107,13 @@ type ClientTransportConfig struct {
|
|||
// If negative, keep-alive probes are disabled.
|
||||
DialServerKeepAlive int64 `json:"dialServerKeepalive,omitempty"`
|
||||
// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
|
||||
// Note: This value only use in TCP/Websocket protocol. Not support in KCP protocol.
|
||||
// Note: This value works with tcp, websocket, http, https, and quic protocols. Not supported with kcp protocol.
|
||||
ConnectServerLocalIP string `json:"connectServerLocalIP,omitempty"`
|
||||
// ConnectServerInterface specifies the network interface name to bind when connecting to server.
|
||||
// Valid values are interface names (e.g., "eth0", "wlan0") or "auto" for auto-detection.
|
||||
// Note: This value works with tcp, websocket, http, https, and quic protocols. Not supported with kcp protocol.
|
||||
// Priority: If both ConnectServerLocalIP and ConnectServerInterface are set, ConnectServerInterface takes precedence.
|
||||
ConnectServerInterface string `json:"connectServerInterface,omitempty"`
|
||||
// ProxyURL specifies a proxy address to connect to the server through. If
|
||||
// this value is "", the server will be connected to directly. By default,
|
||||
// this value is read from the "http_proxy" environment variable.
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/featuregate"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
)
|
||||
|
||||
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
|
@ -88,6 +89,10 @@ func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
|||
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
||||
}
|
||||
|
||||
if err := validateInterfaceBinding(&c.Transport); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
|
||||
for _, f := range c.IncludeConfigFiles {
|
||||
absDir, err := filepath.Abs(filepath.Dir(f))
|
||||
if err != nil {
|
||||
|
@ -124,3 +129,26 @@ func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfi
|
|||
}
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
func validateInterfaceBinding(c *v1.ClientTransportConfig) error {
|
||||
// Check if both interface and IP are specified (they are mutually exclusive)
|
||||
if c.ConnectServerInterface != "" && c.ConnectServerLocalIP != "" {
|
||||
return fmt.Errorf("cannot specify both transport.connectServerInterface and transport.connectServerLocalIP")
|
||||
}
|
||||
|
||||
// Validate interface name if specified
|
||||
if c.ConnectServerInterface != "" {
|
||||
if err := netpkg.ValidateInterfaceOrIP(c.ConnectServerInterface); err != nil {
|
||||
return fmt.Errorf("invalid transport.connectServerInterface: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate IP address if specified
|
||||
if c.ConnectServerLocalIP != "" {
|
||||
if err := netpkg.ValidateInterfaceOrIP(c.ConnectServerLocalIP); err != nil {
|
||||
return fmt.Errorf("invalid transport.connectServerLocalIP: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
// Copyright 2025 Satyajeet Singh, jeet.0733@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InterfaceInfo represents information about a network interface
|
||||
type InterfaceInfo struct {
|
||||
Name string
|
||||
IP string
|
||||
Addr net.Addr
|
||||
}
|
||||
|
||||
// GetInterfaceIP resolves an interface name to its primary IPv4 address
|
||||
func GetInterfaceIP(interfaceName string) (string, error) {
|
||||
if interfaceName == "" {
|
||||
return "", fmt.Errorf("interface name cannot be empty")
|
||||
}
|
||||
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("interface '%s' not found: %v", interfaceName, err)
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get addresses for interface '%s': %v", interfaceName, err)
|
||||
}
|
||||
|
||||
// Look for IPv4 address first
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
if ip := ipnet.IP.To4(); ip != nil && !ip.IsLoopback() {
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no IPv4, look for IPv6
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
if ip := ipnet.IP.To16(); ip != nil && !ip.IsLoopback() {
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("interface '%s' has no valid IP address assigned", interfaceName)
|
||||
}
|
||||
|
||||
// ListNetworkInterfaces returns all available network interfaces with their IP addresses
|
||||
func ListNetworkInterfaces() ([]InterfaceInfo, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to enumerate network interfaces: %v", err)
|
||||
}
|
||||
|
||||
var result []InterfaceInfo
|
||||
for _, iface := range interfaces {
|
||||
// Skip down interfaces
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue // Skip interfaces with address errors
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
// Skip loopback addresses
|
||||
if ipnet.IP.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, InterfaceInfo{
|
||||
Name: iface.Name,
|
||||
IP: ipnet.IP.String(),
|
||||
Addr: addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetFirstNonLoopbackIP returns the first available non-loopback IPv4 address
|
||||
func GetFirstNonLoopbackIP() (string, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to enumerate network interfaces: %v", err)
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
// Skip down interfaces
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||
if ip := ipnet.IP.To4(); ip != nil && !ip.IsLoopback() {
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no suitable network interface found")
|
||||
}
|
||||
|
||||
// ValidateInterfaceOrIP validates if the given value is a valid interface name or IP address
|
||||
func ValidateInterfaceOrIP(value string) error {
|
||||
if value == "" {
|
||||
return fmt.Errorf("value cannot be empty")
|
||||
}
|
||||
|
||||
// Check if it's a valid IP address
|
||||
if ip := net.ParseIP(value); ip != nil {
|
||||
if ip.IsLoopback() {
|
||||
return fmt.Errorf("loopback IP address '%s' is not allowed", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if it's "auto" (special keyword)
|
||||
if value == "auto" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if it's a valid interface name
|
||||
if _, err := net.InterfaceByName(value); err != nil {
|
||||
return fmt.Errorf("'%s' is not a valid interface name or IP address", value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveBindAddress resolves the binding address from interface name or IP address
|
||||
func ResolveBindAddress(value string) (string, error) {
|
||||
if value == "" {
|
||||
return "", nil // No binding specified
|
||||
}
|
||||
|
||||
if value == "auto" {
|
||||
return GetFirstNonLoopbackIP()
|
||||
}
|
||||
|
||||
// Check if it's already an IP address
|
||||
if ip := net.ParseIP(value); ip != nil {
|
||||
if ip.IsLoopback() {
|
||||
return "", fmt.Errorf("loopback IP address '%s' is not allowed", value)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Treat as interface name
|
||||
return GetInterfaceIP(value)
|
||||
}
|
||||
|
||||
// GetAvailableInterfaces returns a formatted string of available interfaces for error messages
|
||||
func GetAvailableInterfaces() string {
|
||||
interfaces, err := ListNetworkInterfaces()
|
||||
if err != nil {
|
||||
return "failed to enumerate interfaces"
|
||||
}
|
||||
|
||||
if len(interfaces) == 0 {
|
||||
return "no interfaces available"
|
||||
}
|
||||
|
||||
var names []string
|
||||
seen := make(map[string]bool)
|
||||
for _, iface := range interfaces {
|
||||
if !seen[iface.Name] {
|
||||
names = append(names, iface.Name)
|
||||
seen[iface.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright 2025 Satyajeet Singh, jeet.0733@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateInterfaceOrIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantErr bool
|
||||
}{
|
||||
{"empty", "", true},
|
||||
{"valid ip", "192.168.1.1", false},
|
||||
{"valid ipv6", "2001:db8::1", false},
|
||||
{"loopback ip", "127.0.0.1", true},
|
||||
{"auto keyword", "auto", false},
|
||||
{"invalid interface", "nonexistent", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateInterfaceOrIP(tt.value)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateInterfaceOrIP() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBindAddress(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"empty", "", "", false},
|
||||
{"auto", "auto", "", false}, // Will return IP if interfaces available, empty if not
|
||||
{"valid ip", "192.168.1.1", "192.168.1.1", false},
|
||||
{"loopback ip", "127.0.0.1", "", true},
|
||||
{"invalid interface", "nonexistent", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ResolveBindAddress(tt.value)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ResolveBindAddress() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
// For "auto" case, we can't predict the exact IP, so just check it's not empty if no error
|
||||
if tt.name == "auto" && err == nil {
|
||||
if got == "" {
|
||||
t.Errorf("ResolveBindAddress() returned empty for auto, expected an IP address")
|
||||
}
|
||||
} else if got != tt.want {
|
||||
t.Errorf("ResolveBindAddress() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNetworkInterfaces(t *testing.T) {
|
||||
interfaces, err := ListNetworkInterfaces()
|
||||
if err != nil {
|
||||
t.Fatalf("ListNetworkInterfaces() error = %v", err)
|
||||
}
|
||||
|
||||
// Should have at least one interface
|
||||
if len(interfaces) == 0 {
|
||||
t.Log("No network interfaces found (this might be normal in some environments)")
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if iface.Name == "" {
|
||||
t.Errorf("Interface name should not be empty")
|
||||
}
|
||||
if iface.IP == "" {
|
||||
t.Errorf("Interface IP should not be empty for interface %s", iface.Name)
|
||||
}
|
||||
if iface.Addr == nil {
|
||||
t.Errorf("Interface address should not be nil for interface %s", iface.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAvailableInterfaces(t *testing.T) {
|
||||
result := GetAvailableInterfaces()
|
||||
if result == "" {
|
||||
t.Log("No available interfaces found (this might be normal in some environments)")
|
||||
}
|
||||
// Should not return an error message
|
||||
if result == "failed to enumerate interfaces" || result == "no interfaces available" {
|
||||
t.Logf("Interface enumeration result: %s", result)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue