mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-25 13:59:08 +01:00
[proxy,client] Add --external-ip-map flag for the embedded client (#8185)
* [proxy] Add --external-ip-map flag for the embedded client The proxy behind a NAT with a forwarded fixed --wg-port could not advertise its public address as an ICE candidate, so peers fell back to relay. Expose the agent's external IP map through embed.Options and a proxy flag (NB_PROXY_EXTERNAL_IP_MAP). * [proxy] Validate --external-ip-map at startup The engine drops every external IP mapping when one fails to parse, so a typo or a space after a comma silently disabled the feature. Move the agent's validator to shared/externalipmap, run it from the proxy at startup, and trim entries first. * [client,proxy] Require an external IP first in --external-ip-map The engine reads the first component as the external IP, so a mapping like iface/10.0.0.5 passed validation and then dropped every mapping.
This commit is contained in:
+2
-71
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -26,16 +25,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/server"
|
"github.com/netbirdio/netbird/client/server"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/shared/externalipmap"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
invalidInputType int = iota
|
|
||||||
ipInputType
|
|
||||||
interfaceInputType
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dnsLabelsFlag = "extra-dns-labels"
|
dnsLabelsFlag = "extra-dns-labels"
|
||||||
|
|
||||||
@@ -106,7 +100,7 @@ func upFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("failed initializing log %v", err)
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validateNATExternalIPs(natExternalIPs)
|
err = externalipmap.Validate(natExternalIPs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -836,36 +830,6 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
|
|||||||
return &loginRequest, nil
|
return &loginRequest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateNATExternalIPs(list []string) error {
|
|
||||||
for _, element := range list {
|
|
||||||
if element == "" {
|
|
||||||
return fmt.Errorf("empty string is not a valid input for %s", externalIPMapFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
subElements := strings.Split(element, "/")
|
|
||||||
if len(subElements) > 2 {
|
|
||||||
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"String\" or \"String/String\"", element, externalIPMapFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(subElements) == 1 && !isValidIP(subElements[0]) {
|
|
||||||
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
last := 0
|
|
||||||
for _, singleElement := range subElements {
|
|
||||||
inputType, err := validateElement(singleElement)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s is not a valid input for %s. it should be an IP string or a network name", singleElement, externalIPMapFlag)
|
|
||||||
}
|
|
||||||
if last == interfaceInputType && inputType == interfaceInputType {
|
|
||||||
return fmt.Errorf("%s is not a valid input for %s. it should not contain two interface names", element, externalIPMapFlag)
|
|
||||||
}
|
|
||||||
last = inputType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInterfaceName(name string) error {
|
func parseInterfaceName(name string) error {
|
||||||
if runtime.GOOS != "darwin" {
|
if runtime.GOOS != "darwin" {
|
||||||
return nil
|
return nil
|
||||||
@@ -878,39 +842,6 @@ func parseInterfaceName(name string) error {
|
|||||||
return fmt.Errorf("invalid interface name %s. Please use the prefix utun followed by a number on MacOS. e.g., utun1 or utun199", name)
|
return fmt.Errorf("invalid interface name %s. Please use the prefix utun followed by a number on MacOS. e.g., utun1 or utun199", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateElement(element string) (int, error) {
|
|
||||||
if isValidIP(element) {
|
|
||||||
return ipInputType, nil
|
|
||||||
}
|
|
||||||
validIface, err := isValidInterface(element)
|
|
||||||
if err != nil {
|
|
||||||
return invalidInputType, fmt.Errorf("unable to validate the network interface name, error: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if validIface {
|
|
||||||
return interfaceInputType, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return interfaceInputType, fmt.Errorf("invalid IP or network interface name not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidIP(ip string) bool {
|
|
||||||
return net.ParseIP(ip) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidInterface(name string) (bool, error) {
|
|
||||||
netInterfaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
for _, iface := range netInterfaces {
|
|
||||||
if iface.Name == name {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseCustomDNSAddress(modified bool) ([]byte, error) {
|
func parseCustomDNSAddress(modified bool) ([]byte, error) {
|
||||||
var parsed []byte
|
var parsed []byte
|
||||||
if modified {
|
if modified {
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ type Options struct {
|
|||||||
MTU *uint16
|
MTU *uint16
|
||||||
// DNSLabels defines additional DNS labels configured in the peer.
|
// DNSLabels defines additional DNS labels configured in the peer.
|
||||||
DNSLabels []string
|
DNSLabels []string
|
||||||
|
// NATExternalIPs maps external IPs to internal IPs or interfaces for ICE
|
||||||
|
// candidates, same format as the agent's --external-ip-map flag.
|
||||||
|
NATExternalIPs []string
|
||||||
// Performance configures the tunnel's buffer pool cap and batch size.
|
// Performance configures the tunnel's buffer pool cap and batch size.
|
||||||
Performance Performance
|
Performance Performance
|
||||||
}
|
}
|
||||||
@@ -224,6 +227,7 @@ func New(opts Options) (*Client, error) {
|
|||||||
WireguardPort: opts.WireguardPort,
|
WireguardPort: opts.WireguardPort,
|
||||||
MTU: opts.MTU,
|
MTU: opts.MTU,
|
||||||
DNSLabels: parsedLabels,
|
DNSLabels: parsedLabels,
|
||||||
|
NATExternalIPs: opts.NATExternalIPs,
|
||||||
}
|
}
|
||||||
if opts.ConfigPath != "" {
|
if opts.ConfigPath != "" {
|
||||||
config, err = profilemanager.UpdateOrCreateConfig(input)
|
config, err = profilemanager.UpdateOrCreateConfig(input)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/acme"
|
"golang.org/x/crypto/acme"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/shared/externalipmap"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
"github.com/netbirdio/netbird/shared/profiling"
|
"github.com/netbirdio/netbird/shared/profiling"
|
||||||
|
|
||||||
@@ -74,6 +76,7 @@ var (
|
|||||||
certLockMethod string
|
certLockMethod string
|
||||||
wildcardCertDir string
|
wildcardCertDir string
|
||||||
wgPort uint16
|
wgPort uint16
|
||||||
|
externalIPMap []string
|
||||||
proxyProtocol bool
|
proxyProtocol bool
|
||||||
preSharedKey string
|
preSharedKey string
|
||||||
supportsCustomPorts bool
|
supportsCustomPorts bool
|
||||||
@@ -117,6 +120,7 @@ func init() {
|
|||||||
rootCmd.Flags().StringVar(&certLockMethod, "cert-lock-method", envStringOrDefault("NB_PROXY_CERT_LOCK_METHOD", "auto"), "Certificate lock method for cross-replica coordination: auto, flock, or k8s-lease")
|
rootCmd.Flags().StringVar(&certLockMethod, "cert-lock-method", envStringOrDefault("NB_PROXY_CERT_LOCK_METHOD", "auto"), "Certificate lock method for cross-replica coordination: auto, flock, or k8s-lease")
|
||||||
rootCmd.Flags().StringVar(&wildcardCertDir, "wildcard-cert-dir", envStringOrDefault("NB_PROXY_WILDCARD_CERT_DIR", ""), "Directory containing wildcard certificate pairs (<name>.crt/<name>.key). Wildcard patterns are extracted from SANs automatically")
|
rootCmd.Flags().StringVar(&wildcardCertDir, "wildcard-cert-dir", envStringOrDefault("NB_PROXY_WILDCARD_CERT_DIR", ""), "Directory containing wildcard certificate pairs (<name>.crt/<name>.key). Wildcard patterns are extracted from SANs automatically")
|
||||||
rootCmd.Flags().Uint16Var(&wgPort, "wg-port", envUint16OrDefault("NB_PROXY_WG_PORT", 0), "WireGuard listen port (0 = random). Fixed port only works with single-account deployments")
|
rootCmd.Flags().Uint16Var(&wgPort, "wg-port", envUint16OrDefault("NB_PROXY_WG_PORT", 0), "WireGuard listen port (0 = random). Fixed port only works with single-account deployments")
|
||||||
|
rootCmd.Flags().StringSliceVar(&externalIPMap, "external-ip-map", envStringSliceOrDefault("NB_PROXY_EXTERNAL_IP_MAP", nil), "Comma-separated external IPs to advertise as ICE candidates, as IP or IP/internal-IP-or-interface (e.g. '203.0.113.5/10.0.0.5'). Requires a fixed --wg-port forwarded on the NAT")
|
||||||
rootCmd.Flags().BoolVar(&proxyProtocol, "proxy-protocol", envBoolOrDefault("NB_PROXY_PROXY_PROTOCOL", false), "Enable PROXY protocol on TCP listeners to preserve client IPs behind L4 proxies")
|
rootCmd.Flags().BoolVar(&proxyProtocol, "proxy-protocol", envBoolOrDefault("NB_PROXY_PROXY_PROTOCOL", false), "Enable PROXY protocol on TCP listeners to preserve client IPs behind L4 proxies")
|
||||||
rootCmd.Flags().StringVar(&preSharedKey, "preshared-key", envStringOrDefault("NB_PROXY_PRESHARED_KEY", ""), "Define a pre-shared key for the tunnel between proxy and peers")
|
rootCmd.Flags().StringVar(&preSharedKey, "preshared-key", envStringOrDefault("NB_PROXY_PRESHARED_KEY", ""), "Define a pre-shared key for the tunnel between proxy and peers")
|
||||||
rootCmd.Flags().BoolVar(&supportsCustomPorts, "supports-custom-ports", envBoolOrDefault("NB_PROXY_SUPPORTS_CUSTOM_PORTS", true), "Whether the proxy can bind arbitrary ports for UDP/TCP passthrough")
|
rootCmd.Flags().BoolVar(&supportsCustomPorts, "supports-custom-ports", envBoolOrDefault("NB_PROXY_SUPPORTS_CUSTOM_PORTS", true), "Whether the proxy can bind arbitrary ports for UDP/TCP passthrough")
|
||||||
@@ -221,6 +225,13 @@ func runServer(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("invalid --trusted-proxies: %w", err)
|
return fmt.Errorf("invalid --trusted-proxies: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range externalIPMap {
|
||||||
|
externalIPMap[i] = strings.TrimSpace(externalIPMap[i])
|
||||||
|
}
|
||||||
|
if err := externalipmap.Validate(externalIPMap); err != nil {
|
||||||
|
return fmt.Errorf("invalid --external-ip-map: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
@@ -248,6 +259,7 @@ func runServer(cmd *cobra.Command, args []string) error {
|
|||||||
CertLockMethod: nbacme.CertLockMethod(certLockMethod),
|
CertLockMethod: nbacme.CertLockMethod(certLockMethod),
|
||||||
WildcardCertDir: wildcardCertDir,
|
WildcardCertDir: wildcardCertDir,
|
||||||
WireguardPort: wgPort,
|
WireguardPort: wgPort,
|
||||||
|
NATExternalIPs: externalIPMap,
|
||||||
Performance: perf,
|
Performance: perf,
|
||||||
ProxyProtocol: proxyProtocol,
|
ProxyProtocol: proxyProtocol,
|
||||||
PreSharedKey: preSharedKey,
|
PreSharedKey: preSharedKey,
|
||||||
@@ -286,6 +298,14 @@ func envStringOrDefault(key string, def string) string {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func envStringSliceOrDefault(key string, def []string) []string {
|
||||||
|
v, exists := os.LookupEnv(key)
|
||||||
|
if !exists || v == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return strings.Split(v, ",")
|
||||||
|
}
|
||||||
|
|
||||||
func envUint16OrDefault(key string, def uint16) uint16 {
|
func envUint16OrDefault(key string, def uint16) uint16 {
|
||||||
v, exists := os.LookupEnv(key)
|
v, exists := os.LookupEnv(key)
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ type ClientConfig struct {
|
|||||||
WGPort uint16
|
WGPort uint16
|
||||||
PreSharedKey string
|
PreSharedKey string
|
||||||
Performance embed.Performance
|
Performance embed.Performance
|
||||||
|
// NATExternalIPs mirrors embed.Options.NATExternalIPs.
|
||||||
|
NATExternalIPs []string
|
||||||
// BlockInbound mirrors embed.Options.BlockInbound. Set to true on the
|
// BlockInbound mirrors embed.Options.BlockInbound. Set to true on the
|
||||||
// standalone proxy where the embedded client never accepts inbound;
|
// standalone proxy where the embedded client never accepts inbound;
|
||||||
// set to false on the private/embedded proxy so the engine creates
|
// set to false on the private/embedded proxy so the engine creates
|
||||||
@@ -402,6 +404,7 @@ func (n *NetBird) createClientEntry(ctx context.Context, accountID types.Account
|
|||||||
WireguardPort: &wgPort,
|
WireguardPort: &wgPort,
|
||||||
PreSharedKey: n.clientCfg.PreSharedKey,
|
PreSharedKey: n.clientCfg.PreSharedKey,
|
||||||
Performance: n.clientCfg.Performance,
|
Performance: n.clientCfg.Performance,
|
||||||
|
NATExternalIPs: n.clientCfg.NATExternalIPs,
|
||||||
}
|
}
|
||||||
logEmbedOptions(n.logger, accountID, serviceID, publicKey.String(), embedOpts)
|
logEmbedOptions(n.logger, accountID, serviceID, publicKey.String(), embedOpts)
|
||||||
client, err := embed.New(embedOpts)
|
client, err := embed.New(embedOpts)
|
||||||
@@ -914,6 +917,7 @@ func logEmbedOptions(logger *log.Logger, accountID types.AccountID, serviceID ty
|
|||||||
"management_url": opts.ManagementURL,
|
"management_url": opts.ManagementURL,
|
||||||
"log_level": opts.LogLevel,
|
"log_level": opts.LogLevel,
|
||||||
"wg_port": wgPort,
|
"wg_port": wgPort,
|
||||||
|
"nat_external_ips": opts.NATExternalIPs,
|
||||||
"mtu": mtu,
|
"mtu": mtu,
|
||||||
"block_inbound": opts.BlockInbound,
|
"block_inbound": opts.BlockInbound,
|
||||||
"block_lan_access": opts.BlockLANAccess,
|
"block_lan_access": opts.BlockLANAccess,
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ type Config struct {
|
|||||||
// WireguardPort is the UDP port for the embedded NetBird tunnel.
|
// WireguardPort is the UDP port for the embedded NetBird tunnel.
|
||||||
// Zero asks the OS for a random port.
|
// Zero asks the OS for a random port.
|
||||||
WireguardPort uint16
|
WireguardPort uint16
|
||||||
|
// NATExternalIPs are external IPs advertised as ICE candidates by the
|
||||||
|
// embedded tunnel. Requires a fixed WireguardPort forwarded on the NAT.
|
||||||
|
NATExternalIPs []string
|
||||||
// ProxyProtocol enables PROXY protocol (v1/v2) on TCP listeners.
|
// ProxyProtocol enables PROXY protocol (v1/v2) on TCP listeners.
|
||||||
ProxyProtocol bool
|
ProxyProtocol bool
|
||||||
// PreSharedKey is the WireGuard pre-shared key used between the
|
// PreSharedKey is the WireGuard pre-shared key used between the
|
||||||
@@ -160,6 +163,7 @@ func New(ctx context.Context, cfg Config) *Server {
|
|||||||
ForwardedProto: cfg.ForwardedProto,
|
ForwardedProto: cfg.ForwardedProto,
|
||||||
TrustedProxies: cfg.TrustedProxies,
|
TrustedProxies: cfg.TrustedProxies,
|
||||||
WireguardPort: cfg.WireguardPort,
|
WireguardPort: cfg.WireguardPort,
|
||||||
|
NATExternalIPs: cfg.NATExternalIPs,
|
||||||
ProxyProtocol: cfg.ProxyProtocol,
|
ProxyProtocol: cfg.ProxyProtocol,
|
||||||
PreSharedKey: cfg.PreSharedKey,
|
PreSharedKey: cfg.PreSharedKey,
|
||||||
Performance: cfg.Performance,
|
Performance: cfg.Performance,
|
||||||
|
|||||||
+8
-4
@@ -203,6 +203,9 @@ type Server struct {
|
|||||||
// single-account deployments; multiple accounts will fail to bind
|
// single-account deployments; multiple accounts will fail to bind
|
||||||
// the same port.
|
// the same port.
|
||||||
WireguardPort uint16
|
WireguardPort uint16
|
||||||
|
// NATExternalIPs maps external IPs advertised as ICE candidates by every
|
||||||
|
// embedded client. Requires a fixed WireguardPort forwarded on the NAT.
|
||||||
|
NATExternalIPs []string
|
||||||
// Performance configures the tunnel pool/batch sizes for every
|
// Performance configures the tunnel pool/batch sizes for every
|
||||||
// embedded client this proxy spawns.
|
// embedded client this proxy spawns.
|
||||||
Performance embed.Performance
|
Performance embed.Performance
|
||||||
@@ -581,10 +584,11 @@ func (s *Server) initManagementClient() error {
|
|||||||
// inbound listeners.
|
// inbound listeners.
|
||||||
func (s *Server) initNetBirdClient() error {
|
func (s *Server) initNetBirdClient() error {
|
||||||
s.netbird = roundtrip.NewNetBird(s.ctx, s.ID, s.ProxyURL, roundtrip.ClientConfig{
|
s.netbird = roundtrip.NewNetBird(s.ctx, s.ID, s.ProxyURL, roundtrip.ClientConfig{
|
||||||
MgmtAddr: s.ManagementAddress,
|
MgmtAddr: s.ManagementAddress,
|
||||||
WGPort: s.WireguardPort,
|
WGPort: s.WireguardPort,
|
||||||
PreSharedKey: s.PreSharedKey,
|
PreSharedKey: s.PreSharedKey,
|
||||||
Performance: s.Performance,
|
Performance: s.Performance,
|
||||||
|
NATExternalIPs: s.NATExternalIPs,
|
||||||
// On --private the embedded client serves per-account inbound
|
// On --private the embedded client serves per-account inbound
|
||||||
// listeners and must apply management's ACL: keep BlockInbound off
|
// listeners and must apply management's ACL: keep BlockInbound off
|
||||||
// so the engine creates the ACL manager. On the standalone proxy
|
// so the engine creates the ACL manager. On the standalone proxy
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// Package externalipmap validates the --external-ip-map flag shared by the
|
||||||
|
// agent and the proxy.
|
||||||
|
package externalipmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const flagName = "external-ip-map"
|
||||||
|
|
||||||
|
// Validate checks each element is formatted as "IP", "IP/IP" or
|
||||||
|
// "IP/interface". The engine drops every mapping when one element fails to
|
||||||
|
// parse, so callers should reject bad input up front.
|
||||||
|
func Validate(list []string) error {
|
||||||
|
for _, element := range list {
|
||||||
|
if element == "" {
|
||||||
|
return fmt.Errorf("empty string is not a valid input for %s", flagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
subElements := strings.Split(element, "/")
|
||||||
|
if len(subElements) > 2 {
|
||||||
|
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"String\" or \"String/String\"", element, flagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidIP(subElements[0]) {
|
||||||
|
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, flagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(subElements) == 2 {
|
||||||
|
if err := validateInternal(subElements[1]); err != nil {
|
||||||
|
return fmt.Errorf("%s is not a valid input for %s. it should be an IP string or a network name: %w", subElements[1], flagName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateInternal(element string) error {
|
||||||
|
if isValidIP(element) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
validIface, err := isValidInterface(element)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate network interface name: %w", err)
|
||||||
|
}
|
||||||
|
if !validIface {
|
||||||
|
return fmt.Errorf("invalid IP or network interface name not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidIP uses net.ParseIP to match the engine's parser exactly.
|
||||||
|
func isValidIP(ip string) bool {
|
||||||
|
return net.ParseIP(ip) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidInterface(name string) (bool, error) {
|
||||||
|
netInterfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, iface := range netInterfaces {
|
||||||
|
if iface.Name == name {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package externalipmap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, ifaces, "host must have at least one interface")
|
||||||
|
iface := ifaces[0].Name
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "nil", input: nil},
|
||||||
|
{name: "single IP", input: []string{"203.0.113.5"}},
|
||||||
|
{name: "IP to IP", input: []string{"203.0.113.5/10.0.0.5"}},
|
||||||
|
{name: "IP to interface", input: []string{"203.0.113.5/" + iface}},
|
||||||
|
{name: "several", input: []string{"203.0.113.5", "203.0.113.6/10.0.0.6"}},
|
||||||
|
{name: "empty element", input: []string{""}, wantErr: true},
|
||||||
|
// The engine rejects padded values, so validation must too.
|
||||||
|
{name: "space padded", input: []string{"203.0.113.5", " 203.0.113.6"}, wantErr: true},
|
||||||
|
{name: "not an IP", input: []string{"not-an-ip"}, wantErr: true},
|
||||||
|
{name: "too many slashes", input: []string{"203.0.113.5/10.0.0.5/10.0.0.6"}, wantErr: true},
|
||||||
|
{name: "unknown interface", input: []string{"203.0.113.5/nb-no-such-iface0"}, wantErr: true},
|
||||||
|
{name: "external interface", input: []string{iface}, wantErr: true},
|
||||||
|
{name: "interface to IP", input: []string{iface + "/10.0.0.5"}, wantErr: true},
|
||||||
|
{name: "two interfaces", input: []string{iface + "/" + iface}, wantErr: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := Validate(tt.input)
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err, "input %q should be rejected", tt.input)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err, "input %q should be accepted", tt.input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user