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"
|
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
|
### Port range mapping
|
||||||
|
|
||||||
*Added in v0.56.0*
|
*Added in v0.56.0*
|
||||||
|
|
|
@ -196,8 +196,14 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
|
||||||
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
|
dialOptions = append(dialOptions, libnet.WithTLSConfig(tlsConfig))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.cfg.Transport.ConnectServerLocalIP != "" {
|
// Resolve binding address from interface name or IP address
|
||||||
dialOptions = append(dialOptions, libnet.WithLocalAddr(c.cfg.Transport.ConnectServerLocalIP))
|
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,
|
dialOptions = append(dialOptions,
|
||||||
libnet.WithProtocol(protocol),
|
libnet.WithProtocol(protocol),
|
||||||
|
@ -214,6 +220,20 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
|
||||||
return conn, err
|
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 {
|
func (c *defaultConnectorImpl) Close() error {
|
||||||
c.closeOnce.Do(func() {
|
c.closeOnce.Do(func() {
|
||||||
if c.quicConn != nil {
|
if c.quicConn != nil {
|
||||||
|
|
|
@ -88,9 +88,16 @@ transport.poolCount = 5
|
||||||
transport.protocol = "tcp"
|
transport.protocol = "tcp"
|
||||||
|
|
||||||
# set client binding ip when connect server, default is empty.
|
# 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"
|
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
|
# 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
|
# it only works when protocol is tcp
|
||||||
# transport.proxyURL = "http://user:passwd@192.168.1.128:8080"
|
# transport.proxyURL = "http://user:passwd@192.168.1.128:8080"
|
||||||
|
|
|
@ -99,9 +99,16 @@ login_fail_exit = true
|
||||||
protocol = tcp
|
protocol = tcp
|
||||||
|
|
||||||
# set client binding ip when connect server, default is empty.
|
# 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
|
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 protocol options
|
||||||
# quic_keepalive_period = 10
|
# quic_keepalive_period = 10
|
||||||
# quic_max_idle_timeout = 30
|
# 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().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.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.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")
|
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||||
}
|
}
|
||||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
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"`
|
DialServerKeepAlive int64 `ini:"dial_server_keepalive" json:"dial_server_keepalive"`
|
||||||
// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
|
// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
|
||||||
// By default, this value is empty.
|
// 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"`
|
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
|
// 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 "", the server will be connected to directly. By default,
|
||||||
// this value is read from the "http_proxy" environment variable.
|
// 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.DialServerTimeout = conf.DialServerTimeout
|
||||||
out.Transport.DialServerKeepAlive = conf.DialServerKeepAlive
|
out.Transport.DialServerKeepAlive = conf.DialServerKeepAlive
|
||||||
out.Transport.ConnectServerLocalIP = conf.ConnectServerLocalIP
|
out.Transport.ConnectServerLocalIP = conf.ConnectServerLocalIP
|
||||||
|
out.Transport.ConnectServerInterface = conf.ConnectServerInterface
|
||||||
out.Transport.ProxyURL = conf.HTTPProxy
|
out.Transport.ProxyURL = conf.HTTPProxy
|
||||||
out.Transport.PoolCount = conf.PoolCount
|
out.Transport.PoolCount = conf.PoolCount
|
||||||
out.Transport.TCPMux = lo.ToPtr(conf.TCPMux)
|
out.Transport.TCPMux = lo.ToPtr(conf.TCPMux)
|
||||||
|
|
|
@ -107,8 +107,13 @@ type ClientTransportConfig struct {
|
||||||
// If negative, keep-alive probes are disabled.
|
// If negative, keep-alive probes are disabled.
|
||||||
DialServerKeepAlive int64 `json:"dialServerKeepalive,omitempty"`
|
DialServerKeepAlive int64 `json:"dialServerKeepalive,omitempty"`
|
||||||
// ConnectServerLocalIP specifies the address of the client bind when it connect to server.
|
// 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"`
|
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
|
// 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 "", the server will be connected to directly. By default,
|
||||||
// this value is read from the "http_proxy" environment variable.
|
// this value is read from the "http_proxy" environment variable.
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/featuregate"
|
"github.com/fatedier/frp/pkg/featuregate"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
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))
|
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 {
|
for _, f := range c.IncludeConfigFiles {
|
||||||
absDir, err := filepath.Abs(filepath.Dir(f))
|
absDir, err := filepath.Abs(filepath.Dir(f))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -124,3 +129,26 @@ func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfi
|
||||||
}
|
}
|
||||||
return warnings, nil
|
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