diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index edebb356d..0b21c6bf5 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -9,8 +9,10 @@ import ( "context" "errors" "fmt" + "net" "os" "runtime" + "strconv" "strings" "sync" "time" @@ -248,7 +250,7 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func }) sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) { for _, pln := range b.peerAPIListeners { - ss.PeerAPIURL = append(ss.PeerAPIURL, "http://"+pln.ln.Addr().String()) + ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr) } }) // TODO: hostinfo, and its networkinfo @@ -1446,8 +1448,14 @@ func (b *LocalBackend) initPeerAPIListener() { } b.peerAPIListeners = nil + var tunName string + if ge, ok := b.e.(wgengine.InternalsGetter); ok { + tunDev, _ := ge.GetInternals() + tunName, _ = tunDev.Name() + } + for _, a := range b.netMap.Addresses { - ln, err := peerAPIListen(a.IP, b.prevIfState) + ln, err := peerAPIListen(a.IP, b.prevIfState, tunName) if err != nil { b.logf("[unexpected] peerAPI listen(%q) error: %v", a.IP, err) continue @@ -1456,6 +1464,8 @@ func (b *LocalBackend) initPeerAPIListener() { ln: ln, lb: b, } + pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.Port())) + go pln.serve() b.peerAPIListeners = append(b.peerAPIListeners, pln) } diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 29bcd0abe..e04429840 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -13,6 +13,7 @@ import ( "io" "net" "net/http" + "runtime" "strconv" "inet.af/netaddr" @@ -20,18 +21,29 @@ import ( "tailscale.com/tailcfg" ) -var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State) error +var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error + +func peerAPIListen(ip netaddr.IP, ifState *interfaces.State, tunIfName string) (ln net.Listener, err error) { + ipStr := ip.String() -func peerAPIListen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) { var lc net.ListenConfig if initListenConfig != nil { // On iOS/macOS, this sets the lc.Control hook to // setsockopt the interface index to bind to, to get // out of the network sandbox. - if err := initListenConfig(&lc, ip, ifState); err != nil { + if err := initListenConfig(&lc, ip, ifState, tunIfName); err != nil { return nil, err } + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + ipStr = "" + } + } + + tcp4or6 := "tcp4" + if ip.Is6() { + tcp4or6 = "tcp6" } + // Make a best effort to pick a deterministic port number for // the ip The lower three bytes are the same for IPv4 and IPv6 // Tailscale addresses (at least currently), so we'll usually @@ -45,18 +57,27 @@ func peerAPIListen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, e hashData := a16[len(a16)-3:] hashData[0] += try tryPort := (32 << 10) | uint16(crc32.ChecksumIEEE(hashData)) - ln, err = lc.Listen(context.Background(), "tcp", net.JoinHostPort(ip.String(), strconv.Itoa(int(tryPort)))) + ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, strconv.Itoa(int(tryPort)))) if err == nil { return ln, nil } } // Fall back to random ephemeral port. - return lc.Listen(context.Background(), "tcp", net.JoinHostPort(ip.String(), "0")) + return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0")) } type peerAPIListener struct { - ln net.Listener - lb *LocalBackend + ln net.Listener + lb *LocalBackend + urlStr string +} + +func (pln *peerAPIListener) Port() int { + ta, ok := pln.ln.Addr().(*net.TCPAddr) + if !ok { + return 0 + } + return ta.Port } func (pln *peerAPIListener) serve() { diff --git a/ipn/ipnlocal/peerapi_macios_ext.go b/ipn/ipnlocal/peerapi_macios_ext.go new file mode 100644 index 000000000..a75e18eed --- /dev/null +++ b/ipn/ipnlocal/peerapi_macios_ext.go @@ -0,0 +1,54 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin,redo ios,redo + +package ipnlocal + +import ( + "fmt" + "log" + "net" + "strings" + "syscall" + + "golang.org/x/sys/unix" + "inet.af/netaddr" + "tailscale.com/net/interfaces" +) + +func init() { + initListenConfig = initListenConfigNetworkExtension +} + +// initListenConfigNetworkExtension configures nc for listening on IP +// through the iOS/macOS Network/System Extension (Packet Tunnel +// Provider) sandbox. +func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *interfaces.State, tunIfName string) error { + tunIf, ok := st.Interface[tunIfName] + if !ok { + return fmt.Errorf("no interface with name %q", tunIfName) + } + nc.Control = func(network, address string, c syscall.RawConn) error { + var sockErr error + err := c.Control(func(fd uintptr) { + + v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6 + proto := unix.IPPROTO_IP + opt := unix.IP_BOUND_IF + if v6 { + proto = unix.IPPROTO_IPV6 + opt = unix.IPV6_BOUND_IF + } + + sockErr = unix.SetsockoptInt(int(fd), proto, opt, tunIf.Index) + log.Printf("peerapi: bind(%q, %q) on index %v = %v", network, address, tunIf.Index, sockErr) + }) + if err != nil { + return err + } + return sockErr + } + return nil +}