Browse Source

wgengine/netstack: in netstack/hybrid mode, fake ICMP using ping command

Change-Id: I42cb4b9b326337f4090d9cea532230e36944b6cb
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 4 years ago
parent
commit
b59e7669c1
1 changed files with 61 additions and 0 deletions
  1. 61 0
      wgengine/netstack/netstack.go

+ 61 - 0
wgengine/netstack/netstack.go

@@ -12,6 +12,8 @@ import (
 	"io"
 	"io"
 	"log"
 	"log"
 	"net"
 	"net"
+	"os/exec"
+	"runtime"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
@@ -35,6 +37,7 @@ import (
 	"tailscale.com/net/tsaddr"
 	"tailscale.com/net/tsaddr"
 	"tailscale.com/net/tsdial"
 	"tailscale.com/net/tsdial"
 	"tailscale.com/net/tstun"
 	"tailscale.com/net/tstun"
+	"tailscale.com/syncs"
 	"tailscale.com/types/logger"
 	"tailscale.com/types/logger"
 	"tailscale.com/types/netmap"
 	"tailscale.com/types/netmap"
 	"tailscale.com/wgengine"
 	"tailscale.com/wgengine"
@@ -377,11 +380,69 @@ func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
 	return false
 	return false
 }
 }
 
 
+var userPingSem = syncs.NewSemaphore(20) // 20 child ping processes at once
+
+// userPing tried to ping dstIP and if it succeeds, injects pingResPkt
+// into the tundev.
+//
+// It's used in userspace/netstack mode when we don't have kernel
+// support or raw socket access. As such, this does the dumbest thing
+// that can work: runs the ping command. It's not super efficient, so
+// it bounds the number of pings going on at once. The idea is that
+// people only use ping occasionally to see if their internet's working
+// so this doesn't need to be great.
+//
+// TODO(bradfitz): when we're running on Windows as the system user, use
+// raw socket APIs instead of ping child processes.
+func (ns *Impl) userPing(dstIP netaddr.IP, pingResPkt []byte) {
+	if !userPingSem.TryAcquire() {
+		return
+	}
+	defer userPingSem.Release()
+
+	t0 := time.Now()
+	var err error
+	switch runtime.GOOS {
+	case "windows":
+		err = exec.Command("ping", "-n", "1", "-w", "3000", dstIP.String()).Run()
+	default:
+		err = exec.Command("ping", "-c", "1", "-W", "3", dstIP.String()).Run()
+	}
+	d := time.Since(t0)
+	if err != nil {
+		ns.logf("exec ping of %v failed in %v", dstIP, d)
+		return
+	}
+	if debugNetstack {
+		ns.logf("exec pinged %v in %v", dstIP, time.Since(t0))
+	}
+	if err := ns.tundev.InjectOutbound(pingResPkt); err != nil {
+		ns.logf("InjectOutbound ping response: %v", err)
+	}
+}
+
 func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
 func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
 	if !ns.shouldProcessInbound(p, t) {
 	if !ns.shouldProcessInbound(p, t) {
 		// Let the host network stack (if any) deal with it.
 		// Let the host network stack (if any) deal with it.
 		return filter.Accept
 		return filter.Accept
 	}
 	}
+
+	destIP := p.Dst.IP()
+	if p.IsEchoRequest() && ns.ProcessSubnets && !tsaddr.IsTailscaleIP(destIP) {
+		var pong []byte // the reply to the ping, if our relayed ping works
+		if destIP.Is4() {
+			h := p.ICMP4Header()
+			h.ToResponse()
+			pong = packet.Generate(&h, p.Payload())
+		} else if destIP.Is6() {
+			h := p.ICMP6Header()
+			h.ToResponse()
+			pong = packet.Generate(&h, p.Payload())
+		}
+		go ns.userPing(destIP, pong)
+		return filter.DropSilently
+	}
+
 	var pn tcpip.NetworkProtocolNumber
 	var pn tcpip.NetworkProtocolNumber
 	switch p.IPVersion {
 	switch p.IPVersion {
 	case 4:
 	case 4: