Skip to content

Commit

Permalink
NET-1315: add public ip change detection thread, remove dependency on…
Browse files Browse the repository at this point in the history
… broker conn lost handler (#810)

* add public ip detection thread

* restart on public ip change

* set proto to zero to get relevant ip

* hard restart on IP changes

* add only ip one voter to debug

* ignore public ip from stun

* update log level from error to warn

* clean up logs, inital ip fetch logic

* add debug log

* fix ipv6 stun dns resolve issue (#812)

* use only stun to discover public ip

* fallback to ip service if stun fails,remove netmaker stun servers

* change log level from error to warn

* exclude InetClient for network change detection (#824)

* fix ipv6 only wgPublicListenPort issue

---------

Co-authored-by: Max Ma <[email protected]>
  • Loading branch information
abhishek9686 and yabinma committed Jun 24, 2024
1 parent bbe25f0 commit 90f01aa
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 59 deletions.
75 changes: 40 additions & 35 deletions functions/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

mqtt "github.com/eclipse/paho.mqtt.golang"
externalip "github.com/glendc/go-external-ip"
"github.com/gravitl/netclient/cache"
"github.com/gravitl/netclient/config"
"github.com/gravitl/netclient/daemon"
Expand Down Expand Up @@ -147,8 +148,6 @@ func startGoRoutines(wg *sync.WaitGroup) context.CancelFunc {
slog.Warn("error reading server map from disk", "error", err)
}
updateConfig := false
config.HostPublicIP, config.WgPublicListenPort, config.HostNatType = holePunchWgPort()
slog.Info("wireguard public listen port: ", "port", config.WgPublicListenPort)

if !config.Netclient().IsStaticPort {
if freeport, err := ncutils.GetFreePort(config.Netclient().ListenPort); err != nil {
Expand All @@ -170,33 +169,35 @@ func startGoRoutines(wg *sync.WaitGroup) context.CancelFunc {
}

if !config.Netclient().IsStatic {
// IPV4
config.HostPublicIP, config.WgPublicListenPort, config.HostNatType = holePunchWgPort(4, config.Netclient().ListenPort)
slog.Info("wireguard public listen port: ", "port", config.WgPublicListenPort)
if config.HostPublicIP != nil && !config.HostPublicIP.IsUnspecified() {
config.Netclient().EndpointIP = config.HostPublicIP
updateConfig = true
} else {
slog.Warn("GetPublicIPv4 error:", "Warn", "no ipv4 found")
config.Netclient().EndpointIP = nil
updateConfig = true
}

if config.Netclient().NatType == "" {
config.Netclient().NatType = config.HostNatType
updateConfig = true
}

ipv6, err := ncutils.GetPublicIPv6()
if err != nil {
slog.Warn("GetPublicIPv6 error: ", "error", err.Error())
} else {
if ipv4 := ipv6.To4(); ipv4 != nil {
slog.Warn("GetPublicIPv6 Warn: ", "Warn", "No IPv6 public ip found")
config.HostPublicIP6 = nil
config.Netclient().EndpointIPv6 = nil
updateConfig = true
} else {
config.Netclient().EndpointIPv6 = ipv6
config.HostPublicIP6 = ipv6
updateConfig = true
// IPV6
publicIP6, wgport, natType := holePunchWgPort(6, config.Netclient().ListenPort)
if publicIP6 != nil && !publicIP6.IsUnspecified() {
config.Netclient().EndpointIPv6 = publicIP6
config.HostPublicIP6 = publicIP6
if config.HostPublicIP == nil {
config.WgPublicListenPort = wgport
config.HostNatType = natType
}
updateConfig = true
} else {
slog.Warn("GetPublicIPv6 Warn: ", "Warn", "no ipv6 found")
config.Netclient().EndpointIPv6 = nil
updateConfig = true
}
}

Expand Down Expand Up @@ -326,10 +327,6 @@ func setupMQTT(server *config.Server) error {
opts.SetResumeSubs(true)
opts.SetConnectionLostHandler(func(c mqtt.Client, e error) {
slog.Warn("detected broker connection lost for", "server", server.Broker)
// restart daemon for new udp hole punch if MQTT connection is lost (can happen on network change)
if !config.Netclient().IsStaticPort || !config.Netclient().IsStatic {
daemon.Restart()
}
})
Mqclient = mqtt.NewClient(opts)
var connecterr error
Expand Down Expand Up @@ -536,26 +533,34 @@ func UpdateKeys() error {
return nil
}

func holePunchWgPort() (pubIP net.IP, pubPort int, natType string) {
func holePunchWgPort(proto, portToStun int) (pubIP net.IP, pubPort int, natType string) {

portToStun := config.Netclient().ListenPort
pubIP, pubPort, natType = stun.HolePunch(portToStun)
if pubIP == nil { // if stun has failed fallback to ip service to get publicIP
var api string
server := config.GetServer(config.CurrServer)
if server != nil {
api = server.API
}
publicIP, err := ncutils.GetPublicIP(api)
pubIP, pubPort, natType = stun.HolePunch(portToStun, proto)
if pubIP == nil || pubIP.IsUnspecified() { // if stun has failed fallback to ip service to get publicIP
publicIP, err := GetPublicIP(uint(proto))
if err != nil {
slog.Error("failed to get publicIP", "error", err)
slog.Warn("failed to get publicIP", "error", err)
return
}
pubIP = publicIP
pubPort = portToStun
}
if ipv4 := pubIP.To4(); ipv4 == nil {
pubIP = nil
}
return
}

func GetPublicIP(proto uint) (net.IP, error) {
// Create the default consensus,
// using the default configuration and no logger.
consensus := externalip.NewConsensus(&externalip.ConsensusConfig{
Timeout: time.Second * 10,
}, nil)
consensus.AddVoter(externalip.NewHTTPSource("https://icanhazip.com/"), 3)
consensus.AddVoter(externalip.NewHTTPSource("https://ifconfig.me/ip"), 3)
consensus.AddVoter(externalip.NewHTTPSource("https://myexternalip.com/raw"), 3)
// By default Ipv4 or Ipv6 is returned,
// use the function below to limit yourself to IPv4,
// or pass in `6` instead to limit yourself to IPv6.
consensus.UseIPProtocol(proto)
// Get your IP,
return consensus.ExternalIP()
}
23 changes: 23 additions & 0 deletions functions/mqpublish.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func Checkin(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
ticker := time.NewTicker(time.Minute * CheckInInterval)
defer ticker.Stop()
ipTicker := time.NewTicker(time.Second * 15)
defer ipTicker.Stop()
for {
select {
case <-ctx.Done():
Expand All @@ -54,7 +56,28 @@ func Checkin(ctx context.Context, wg *sync.WaitGroup) {
continue
}
checkin()
case <-ipTicker.C:
// this ticker is used to detect network changes, and publish new public ip to peers
// if config.Netclient().CurrGwNmIP is not nil, it's an InetClient, then it skips the network change detection
if !config.Netclient().IsStatic && config.Netclient().CurrGwNmIP == nil {
restart := false
ip4, _, _ := holePunchWgPort(4, 0)
if ip4 != nil && !ip4.IsUnspecified() && !config.HostPublicIP.Equal(ip4) {
slog.Warn("IP CHECKIN", "ipv4", ip4, "HostPublicIP", config.HostPublicIP)
restart = true
}
ip6, _, _ := holePunchWgPort(6, 0)
if ip6 != nil && !ip6.IsUnspecified() && !config.HostPublicIP6.Equal(ip6) {
slog.Warn("IP CHECKIN", "ipv6", ip6, "HostPublicIP6", config.HostPublicIP6)
restart = true
}
if restart {
logger.Log(0, "restarting netclient due to network changes...")
daemon.HardRestart()
}
}
}

}
}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/coreos/go-iptables v0.7.0
github.com/devilcove/httpclient v0.6.0
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/glendc/go-external-ip v0.1.0
github.com/go-ping/ping v1.1.0
github.com/google/nftables v0.1.0
github.com/google/uuid v1.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/glendc/go-external-ip v0.1.0 h1:iX3xQ2Q26atAmLTbd++nUce2P5ht5P4uD4V7caSY/xg=
github.com/glendc/go-external-ip v0.1.0/go.mod h1:CNx312s2FLAJoWNdJWZ2Fpf5O4oLsMFwuYviHjS4uJE=
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
Expand Down
72 changes: 48 additions & 24 deletions stun/stun.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ var (
StunServers = []StunServer{
{Domain: "stun1.l.google.com", Port: 19302},
{Domain: "stun2.l.google.com", Port: 19302},
{Domain: "stun1.netmaker.io", Port: 3478},
{Domain: "stun2.netmaker.io", Port: 3478},
{Domain: "stun3.l.google.com", Port: 19302},
{Domain: "stun4.l.google.com", Port: 19302},
}
)

Expand Down Expand Up @@ -58,41 +58,65 @@ func DoesIPExistLocally(ip net.IP) bool {
}

// HolePunch - performs udp hole punching on the given port
func HolePunch(portToStun int) (publicIP net.IP, publicPort int, natType string) {
func HolePunch(portToStun, proto int) (publicIP net.IP, publicPort int, natType string) {
for _, stunServer := range StunServers {
stunServer := stunServer
s, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", stunServer.Domain, stunServer.Port))
if err != nil {
logger.Log(1, "failed to resolve udp addr: ", err.Error())
continue
}
l := &net.UDPAddr{
IP: net.ParseIP(""),
Port: portToStun,
}
slog.Debug(fmt.Sprintf("hole punching port %d via stun server %s:%d", portToStun, stunServer.Domain, stunServer.Port))
publicIP, publicPort, natType, err = doStunTransaction(l, s)
if err != nil {
logger.Log(3, "stun transaction failed: ", stunServer.Domain, err.Error())
continue
}
if publicPort == 0 || publicIP == nil || publicIP.IsUnspecified() {
continue
var err error
if proto == 4 {
publicIP, publicPort, natType, err = callHolePunch(stunServer, portToStun, "udp4")
if err != nil {
slog.Warn("callHolePunch udp4 error", err.Error())
}
} else {
publicIP, publicPort, natType, err = callHolePunch(stunServer, portToStun, "udp6")
if err != nil {
slog.Warn("callHolePunch udp6 error", err.Error())
continue
}
}
break
}
slog.Debug("hole punching complete", "public ip", publicIP.String(), "public port", strconv.Itoa(publicPort))
slog.Debug("hole punching complete", "public ip", publicIP.String(), "public port", strconv.Itoa(publicPort), "nat type", natType)
return
}

func callHolePunch(stunServer StunServer, portToStun int, network string) (publicIP net.IP, publicPort int, natType string, err error) {
s, err := net.ResolveUDPAddr(network, fmt.Sprintf("%s:%d", stunServer.Domain, stunServer.Port))
if err != nil {
logger.Log(1, "failed to resolve udp addr: ", network, err.Error())
return nil, 0, "", err
}
l := &net.UDPAddr{
IP: net.ParseIP(""),
Port: portToStun,
}
slog.Debug(fmt.Sprintf("hole punching port %d via stun server %s:%d", portToStun, stunServer.Domain, stunServer.Port))
publicIP, publicPort, natType, err = doStunTransaction(l, s)
if err != nil {
logger.Log(3, "stun transaction failed: ", stunServer.Domain, err.Error())
return nil, 0, "", err
}

return
}

func doStunTransaction(lAddr, rAddr *net.UDPAddr) (publicIP net.IP, publicPort int, natType string, err error) {
conn, err := net.DialUDP("udp", lAddr, rAddr)
if err != nil {
logger.Log(0, "failed to dial: ", err.Error())
logger.Log(1, "failed to dial: ", err.Error())
return
}
re := strings.Split(conn.LocalAddr().String(), ":")
privIp := net.ParseIP(re[0])

re := conn.LocalAddr().String()
lIP := re[0:strings.LastIndex(re, ":")]
if strings.ContainsAny(lIP, "[") {
lIP = strings.ReplaceAll(lIP, "[", "")
}
if strings.ContainsAny(lIP, "]") {
lIP = strings.ReplaceAll(lIP, "]", "")
}

privIp := net.ParseIP(lIP)
defer func() {
if !privIp.Equal(publicIP) {
natType = nmmodels.NAT_Types.BehindNAT
Expand Down

0 comments on commit 90f01aa

Please sign in to comment.