diff --git a/.gitignore b/.gitignore index 82d4df0d1..796018764 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ infrastructure_files/management.json infrastructure_files/docker-compose.yml *.syso client/.distfiles/ -infrastructure_files/setup.env \ No newline at end of file +infrastructure_files/setup.env +.vscode diff --git a/client/internal/config.go b/client/internal/config.go index 2352cf656..1ba0389ce 100644 --- a/client/internal/config.go +++ b/client/internal/config.go @@ -3,6 +3,9 @@ package internal import ( "context" "fmt" + "net/url" + "os" + "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/iface" mgm "github.com/netbirdio/netbird/management/client" @@ -11,8 +14,6 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "net/url" - "os" ) var managementURLDefault *url.URL @@ -40,7 +41,24 @@ type Config struct { WgPort int IFaceBlackList []string // SSHKey is a private SSH key in a PEM format - SSHKey string + SSHKey string + + // ExternalIP mappings, if different than the host interface IP + // + // External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP + // to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT + // mapping ExternalIP to host interface IP, or a NAT DMZ to host interface IP. + // + // A single mapping will take the form of: external[/internal] + // external (required): either the external IP address or "stun" to use STUN to determine the external IP address + // internal (optional): either the internal/interface IP address or an interface name + // + // examples: + // "12.34.56.78" => all interfaces IPs will be mapped to external IP of 12.34.56.78 + // "12.34.56.78/eth0" => IPv4 assigned to interface eth0 will be mapped to external IP of 12.34.56.78 + // "12.34.56.78/10.1.2.3" => interface IP 10.1.2.3 will be mapped to external IP of 12.34.56.78 + + NATExternalIPs []string } // createNewConfig creates a new config generating a new Wireguard key and saving to file diff --git a/client/internal/connect.go b/client/internal/connect.go index 44bc54fd8..d9ddf5d26 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -3,11 +3,12 @@ package internal import ( "context" "fmt" - "github.com/netbirdio/netbird/client/ssh" - nbStatus "github.com/netbirdio/netbird/client/status" "strings" "time" + "github.com/netbirdio/netbird/client/ssh" + nbStatus "github.com/netbirdio/netbird/client/status" + "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/iface" @@ -190,6 +191,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe WgPrivateKey: key, WgPort: config.WgPort, SSHKey: []byte(config.SSHKey), + NATExternalIPs: config.NATExternalIPs, } if config.PreSharedKey != "" { diff --git a/client/internal/engine.go b/client/internal/engine.go index 7f7e52de5..9099d6908 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -3,12 +3,6 @@ package internal import ( "context" "fmt" - "github.com/netbirdio/netbird/client/internal/dns" - "github.com/netbirdio/netbird/client/internal/routemanager" - nbssh "github.com/netbirdio/netbird/client/ssh" - nbstatus "github.com/netbirdio/netbird/client/status" - nbdns "github.com/netbirdio/netbird/dns" - "github.com/netbirdio/netbird/route" "math/rand" "net" "net/netip" @@ -18,6 +12,13 @@ import ( "sync" "time" + "github.com/netbirdio/netbird/client/internal/dns" + "github.com/netbirdio/netbird/client/internal/routemanager" + nbssh "github.com/netbirdio/netbird/client/ssh" + nbstatus "github.com/netbirdio/netbird/client/status" + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/proxy" "github.com/netbirdio/netbird/iface" @@ -66,6 +67,8 @@ type EngineConfig struct { // SSHKey is a private SSH key in a PEM format SSHKey []byte + + NATExternalIPs []string } // Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers. @@ -825,6 +828,7 @@ func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, er UDPMuxSrflx: e.udpMuxSrflx, ProxyConfig: proxyConfig, LocalWgPort: e.config.WgPort, + NATExternalIPs: e.parseNATExternalIPMappings(), } peerConn, err := peer.NewConn(config, e.statusRecorder) @@ -918,3 +922,77 @@ func (e *Engine) receiveSignalEvents() { e.signal.WaitStreamConnected() } + +func (e* Engine) parseNATExternalIPMappings() []string { + var mappedIPs []string + var ignoredIFaces = make(map[string]interface{}) + for _, iFace := range(e.config.IFaceBlackList) { + ignoredIFaces[iFace] = nil + } + for _, mapping := range e.config.NATExternalIPs { + var external, internal string + var externalIP, internalIP net.IP + var err error + split := strings.Split(mapping, "/") + if len(split) > 2 { + log.Warnf("ignoring invalid external mapping '%s', too many delimiters", mapping) + break + } + if len(split) > 1 { + internal = split[1] + internalIP = net.ParseIP(internal) + if internalIP == nil { + // not a properly formatted IP address, maybe it's interface name? + if _, present := ignoredIFaces[internal]; present { + log.Warnf("internal interface '%s' in blacklist, ignoring external mapping '%s'", internal, mapping) + break + } + internalIP, err = findIPFromInterfaceName(internal) + if err != nil { + log.Warnf("error finding interface IP for interface '%s', ignoring external mapping '%s': %v", internal, mapping, err) + break + } + } + } + external = split[0] + externalIP = net.ParseIP(external) + if externalIP == nil { + log.Warnf("invalid external IP, ignoring external IP mapping '%s'", mapping) + break + } + if externalIP != nil { + mappedIP := externalIP.String() + if internalIP != nil { + mappedIP = mappedIP + "/" + internalIP.String() + } + mappedIPs = append(mappedIPs, mappedIP) + log.Infof("parsed external IP mapping of '%s' as '%s'", mapping, mappedIP) + } + } + if len(mappedIPs) != len(e.config.NATExternalIPs) { + log.Warnf("one or more external IP mappings failed to parse, ignoring all mappings") + return nil + } + return mappedIPs +} + +func findIPFromInterfaceName(ifaceName string) (net.IP, error) { + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return nil, err + } + return findIPFromInterface(iface) +} + +func findIPFromInterface(iface *net.Interface) (net.IP, error) { + ifaceAddrs, err := iface.Addrs() + if err != nil { + return nil, err + } + for _, addr := range ifaceAddrs { + if ipv4Addr := addr.(*net.IPNet).IP.To4(); ipv4Addr != nil { + return ipv4Addr, nil + } + } + return nil, fmt.Errorf("interface %s don't have an ipv4 address", iface.Name) +} \ No newline at end of file diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 1bb6bf823..2f097c156 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -2,18 +2,18 @@ package peer import ( "context" - nbStatus "github.com/netbirdio/netbird/client/status" - "github.com/netbirdio/netbird/client/system" - "github.com/netbirdio/netbird/iface" - "golang.zx2c4.com/wireguard/wgctrl" "net" "strings" "sync" "time" "github.com/netbirdio/netbird/client/internal/proxy" + nbStatus "github.com/netbirdio/netbird/client/status" + "github.com/netbirdio/netbird/client/system" + "github.com/netbirdio/netbird/iface" "github.com/pion/ice/v2" log "github.com/sirupsen/logrus" + "golang.zx2c4.com/wireguard/wgctrl" ) // ConnConfig is a peer Connection configuration @@ -39,6 +39,8 @@ type ConnConfig struct { UDPMuxSrflx ice.UniversalUDPMux LocalWgPort int + + NATExternalIPs []string } // OfferAnswer represents a session establishment offer or answer @@ -152,6 +154,7 @@ func (conn *Conn) reCreateAgent() error { InterfaceFilter: interfaceFilter(conn.config.InterfaceBlackList), UDPMux: conn.config.UDPMux, UDPMuxSrflx: conn.config.UDPMuxSrflx, + NAT1To1IPs: conn.config.NATExternalIPs, }) if err != nil { return err @@ -284,7 +287,7 @@ func (conn *Conn) Open() error { host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String()) rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String()) // direct Wireguard connection - log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, iface.DefaultWgPort, rhost, iface.DefaultWgPort) + log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, conn.config.LocalWgPort, rhost, remoteWgPort) } else { log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String()) } @@ -448,6 +451,7 @@ func (conn *Conn) SetSignalCandidate(handler func(candidate ice.Candidate) error // and then signals them to the remote peer func (conn *Conn) onICECandidate(candidate ice.Candidate) { if candidate != nil { + // TODO: reported port is incorrect for CandidateTypeHost, makes understanding ICE use via logs confusing as port is ignored log.Debugf("discovered local candidate %s", candidate.String()) go func() { err := conn.signalCandidate(candidate)