Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1185457491 | ||
|
|
22d5a8fda5 | ||
|
|
a9818915a8 | ||
|
|
50aa6ed5e4 | ||
|
|
0345649d05 | ||
|
|
facdc475bb | ||
|
|
949c400302 | ||
|
|
0f2a786097 | ||
|
|
1a4f0f50a7 | ||
|
|
66cc9e6301 | ||
|
|
78584a8552 | ||
|
|
bd14112d0b | ||
|
|
b14e31831a | ||
|
|
0b00b7a135 |
@@ -1 +1 @@
|
||||
1.29.0
|
||||
1.30.2
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
@@ -264,13 +265,16 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
}
|
||||
|
||||
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
|
||||
fin, err := os.Open(policyFname)
|
||||
data, err := os.ReadFile(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err = hujson.Standardize(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), fin)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.tailscale.com/api/v2/tailnet/%s/acl/validate", tailnet), bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -19,24 +19,27 @@ var licensesCmd = &ffcli.Command{
|
||||
Exec: runLicenses,
|
||||
}
|
||||
|
||||
func runLicenses(ctx context.Context, args []string) error {
|
||||
var licenseURL string
|
||||
// licensesURL returns the absolute URL containing open source license information for the current platform.
|
||||
func licensesURL() string {
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
licenseURL = "https://tailscale.com/licenses/android"
|
||||
return "https://tailscale.com/licenses/android"
|
||||
case "darwin", "ios":
|
||||
licenseURL = "https://tailscale.com/licenses/apple"
|
||||
return "https://tailscale.com/licenses/apple"
|
||||
case "windows":
|
||||
licenseURL = "https://tailscale.com/licenses/windows"
|
||||
return "https://tailscale.com/licenses/windows"
|
||||
default:
|
||||
licenseURL = "https://tailscale.com/licenses/tailscale"
|
||||
return "https://tailscale.com/licenses/tailscale"
|
||||
}
|
||||
}
|
||||
|
||||
func runLicenses(ctx context.Context, args []string) error {
|
||||
licenses := licensesURL()
|
||||
outln(`
|
||||
Tailscale wouldn't be possible without the contributions of thousands of open
|
||||
source developers. To see the open source packages included in Tailscale and
|
||||
their respective license information, visit:
|
||||
|
||||
` + licenseURL)
|
||||
` + licenses)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ type tmplData struct {
|
||||
IP string
|
||||
AdvertiseExitNode bool
|
||||
AdvertiseRoutes string
|
||||
LicensesURL string
|
||||
}
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
@@ -392,6 +393,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Profile: profile,
|
||||
Status: st.BackendState,
|
||||
DeviceName: deviceName,
|
||||
LicensesURL: licensesURL(),
|
||||
}
|
||||
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</head>
|
||||
|
||||
<body class="py-14">
|
||||
<main class="container max-w-lg mx-auto py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
|
||||
<main class="container max-w-lg mx-auto mb-8 py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
|
||||
<header class="flex justify-between items-center min-width-0 py-2 mb-8">
|
||||
<svg width="26" height="26" viewBox="0 0 23 23" title="Tailscale" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
class="flex-shrink-0 mr-4">
|
||||
@@ -100,6 +100,9 @@
|
||||
</div>
|
||||
{{ end }}
|
||||
</main>
|
||||
<footer class="container max-w-lg mx-auto text-center">
|
||||
<a class="text-xs text-gray-500 hover:text-gray-600" href="{{ .LicensesURL }}">Open Source Licenses</a>
|
||||
</footer>
|
||||
<script>(function () {
|
||||
const advertiseExitNode = {{.AdvertiseExitNode}};
|
||||
let fetchingUrl = false;
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
#! /bin/sh
|
||||
|
||||
set -m # enable job control
|
||||
|
||||
export PATH=$PATH:/tailscale/bin
|
||||
|
||||
TS_AUTH_KEY="${TS_AUTH_KEY:-}"
|
||||
@@ -60,8 +58,16 @@ if [[ ! -z "${TS_TAILSCALED_EXTRA_ARGS}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} ${TS_TAILSCALED_EXTRA_ARGS}"
|
||||
fi
|
||||
|
||||
handler() {
|
||||
echo "Caught SIGINT/SIGTERM, shutting down tailscaled"
|
||||
kill -s SIGINT $PID
|
||||
wait ${PID}
|
||||
}
|
||||
|
||||
echo "Starting tailscaled"
|
||||
tailscaled ${TAILSCALED_ARGS} &
|
||||
PID=$!
|
||||
trap handler SIGINT SIGTERM
|
||||
|
||||
UP_ARGS="--accept-dns=${TS_ACCEPT_DNS}"
|
||||
if [[ ! -z "${TS_ROUTES}" ]]; then
|
||||
@@ -82,4 +88,5 @@ if [[ ! -z "${TS_DEST_IP}" ]]; then
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
|
||||
fi
|
||||
|
||||
fg
|
||||
echo "Waiting for tailscaled to exit"
|
||||
wait ${PID}
|
||||
@@ -1 +1 @@
|
||||
6dca83b256c7decd3dd6706ee47e04f21a0b935c
|
||||
b13188dd36c1ad2509796ce10b6a1231b200c36a
|
||||
|
||||
@@ -183,6 +183,13 @@ func inContainer() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||
return true
|
||||
}
|
||||
if _, err := os.Stat("/run/.containerenv"); err == nil {
|
||||
// See https://github.com/cri-o/cri-o/issues/5461
|
||||
return true
|
||||
}
|
||||
var ret bool
|
||||
lineread.File("/proc/1/cgroup", func(line []byte) error {
|
||||
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
|
||||
|
||||
@@ -205,7 +205,7 @@ func (m *resolvedManager) run(ctx context.Context) {
|
||||
// When ctx goes away systemd-resolved auto reverts.
|
||||
// Keeping for potential use in future refactor.
|
||||
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil {
|
||||
m.logf("[v1] RevertLink: %w", call.Err)
|
||||
m.logf("[v1] RevertLink: %v", call.Err)
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
@@ -57,9 +57,6 @@ func truncatedFlagSet(pkt []byte) bool {
|
||||
}
|
||||
|
||||
const (
|
||||
// responseTimeout is the maximal amount of time to wait for a DNS response.
|
||||
responseTimeout = 5 * time.Second
|
||||
|
||||
// dohTransportTimeout is how long to keep idle HTTP
|
||||
// connections open to DNS-over-HTTPs servers. This is pretty
|
||||
// arbitrary.
|
||||
|
||||
@@ -208,7 +208,7 @@ func ParseResponse(b []byte) (tID TxID, addr netip.AddrPort, err error) {
|
||||
b = b[:attrsLen] // trim trailing packet bytes
|
||||
}
|
||||
|
||||
var addr6, fallbackAddr, fallbackAddr6 netip.AddrPort
|
||||
var fallbackAddr netip.AddrPort
|
||||
|
||||
// Read through the attributes.
|
||||
// The the addr+port reported by XOR-MAPPED-ADDRESS
|
||||
@@ -218,24 +218,20 @@ func ParseResponse(b []byte) (tID TxID, addr netip.AddrPort, err error) {
|
||||
if err := foreachAttr(b, func(attrType uint16, attr []byte) error {
|
||||
switch attrType {
|
||||
case attrXorMappedAddress, attrXorMappedAddressAlt:
|
||||
a, p, err := xorMappedAddress(tID, attr)
|
||||
ipSlice, port, err := xorMappedAddress(tID, attr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(a) == 16 {
|
||||
addr6 = netip.AddrPortFrom(netip.AddrFrom16(*(*[16]byte)([]byte(a))), p)
|
||||
} else {
|
||||
addr = netip.AddrPortFrom(netip.AddrFrom4(*(*[4]byte)([]byte(a))), p)
|
||||
if ip, ok := netip.AddrFromSlice(ipSlice); ok {
|
||||
addr = netip.AddrPortFrom(ip.Unmap(), port)
|
||||
}
|
||||
case attrMappedAddress:
|
||||
a, p, err := mappedAddress(attr)
|
||||
ipSlice, port, err := mappedAddress(attr)
|
||||
if err != nil {
|
||||
return ErrMalformedAttrs
|
||||
}
|
||||
if len(a) == 16 {
|
||||
fallbackAddr6 = netip.AddrPortFrom(netip.AddrFrom16(*(*[16]byte)([]byte(a))), p)
|
||||
} else {
|
||||
fallbackAddr = netip.AddrPortFrom(netip.AddrFrom4(*(*[4]byte)([]byte(a))), p)
|
||||
if ip, ok := netip.AddrFromSlice(ipSlice); ok {
|
||||
fallbackAddr = netip.AddrPortFrom(ip.Unmap(), port)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -250,12 +246,6 @@ func ParseResponse(b []byte) (tID TxID, addr netip.AddrPort, err error) {
|
||||
if fallbackAddr.IsValid() {
|
||||
return tID, fallbackAddr, nil
|
||||
}
|
||||
if addr6.IsValid() {
|
||||
return tID, addr6, nil
|
||||
}
|
||||
if fallbackAddr6.IsValid() {
|
||||
return tID, fallbackAddr6, nil
|
||||
}
|
||||
return tID, netip.AddrPort{}, ErrMalformedAttrs
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ package stun_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
// TODO(bradfitz): fuzz this.
|
||||
@@ -175,6 +177,13 @@ var responseTests = []struct {
|
||||
wantAddr: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
|
||||
wantPort: 61300,
|
||||
},
|
||||
{
|
||||
name: "no-4in6",
|
||||
data: must.Get(hex.DecodeString("010100182112a4424fd5d202dcb37d31fc773306002000140002cd3d2112a4424fd5d202dcb382ce2dc3fcc7")),
|
||||
wantTID: []byte{79, 213, 210, 2, 220, 179, 125, 49, 252, 119, 51, 6},
|
||||
wantAddr: netip.AddrFrom4([4]byte{209, 180, 207, 193}),
|
||||
wantPort: 60463,
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseResponse(t *testing.T) {
|
||||
|
||||
@@ -172,6 +172,11 @@ func printEndpointHTML(w io.Writer, ep *endpoint) {
|
||||
break
|
||||
}
|
||||
pos := (int(s.recentPong) - i) % len(s.recentPongs)
|
||||
// If s.recentPongs wraps around pos will be negative, so start
|
||||
// again from the end of the slice.
|
||||
if pos < 0 {
|
||||
pos += len(s.recentPongs)
|
||||
}
|
||||
pr := s.recentPongs[pos]
|
||||
fmt.Fprintf(w, "<li>pong %v ago: in %v, from %v src %v</li>\n",
|
||||
fmtMono(pr.pongAt), pr.latency.Round(time.Millisecond/10),
|
||||
|
||||
@@ -743,47 +743,64 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
ns.removeSubnetAddress(dialIP)
|
||||
}
|
||||
}()
|
||||
|
||||
var wq waiter.Queue
|
||||
ep, err := r.CreateEndpoint(&wq)
|
||||
if err != nil {
|
||||
ns.logf("CreateEndpoint error for %s: %v", stringifyTEI(reqDetails), err)
|
||||
r.Complete(true) // sends a RST
|
||||
return
|
||||
|
||||
// We can't actually create the endpoint or complete the inbound
|
||||
// request until we're sure that the connection can be handled by this
|
||||
// endpoint. This function sets up the TCP connection and should be
|
||||
// called immediately before a connection is handled.
|
||||
createConn := func() *gonet.TCPConn {
|
||||
ep, err := r.CreateEndpoint(&wq)
|
||||
if err != nil {
|
||||
ns.logf("CreateEndpoint error for %s: %v", stringifyTEI(reqDetails), err)
|
||||
r.Complete(true) // sends a RST
|
||||
return nil
|
||||
}
|
||||
r.Complete(false)
|
||||
|
||||
// SetKeepAlive so that idle connections to peers that have forgotten about
|
||||
// the connection or gone completely offline eventually time out.
|
||||
// Applications might be setting this on a forwarded connection, but from
|
||||
// userspace we can not see those, so the best we can do is to always
|
||||
// perform them with conservative timing.
|
||||
// TODO(tailscale/tailscale#4522): Netstack defaults match the Linux
|
||||
// defaults, and results in a little over two hours before the socket would
|
||||
// be closed due to keepalive. A shorter default might be better, or seeking
|
||||
// a default from the host IP stack. This also might be a useful
|
||||
// user-tunable, as in userspace mode this can have broad implications such
|
||||
// as lingering connections to fork style daemons. On the other side of the
|
||||
// fence, the long duration timers are low impact values for battery powered
|
||||
// peers.
|
||||
ep.SocketOptions().SetKeepAlive(true)
|
||||
|
||||
// The ForwarderRequest.CreateEndpoint above asynchronously
|
||||
// starts the TCP handshake. Note that the gonet.TCPConn
|
||||
// methods c.RemoteAddr() and c.LocalAddr() will return nil
|
||||
// until the handshake actually completes. But we have the
|
||||
// remote address in reqDetails instead, so we don't use
|
||||
// gonet.TCPConn.RemoteAddr. The byte copies in both
|
||||
// directions to/from the gonet.TCPConn in forwardTCP will
|
||||
// block until the TCP handshake is complete.
|
||||
return gonet.NewTCPConn(&wq, ep)
|
||||
}
|
||||
r.Complete(false)
|
||||
|
||||
// SetKeepAlive so that idle connections to peers that have forgotten about
|
||||
// the connection or gone completely offline eventually time out.
|
||||
// Applications might be setting this on a forwarded connection, but from
|
||||
// userspace we can not see those, so the best we can do is to always
|
||||
// perform them with conservative timing.
|
||||
// TODO(tailscale/tailscale#4522): Netstack defaults match the Linux
|
||||
// defaults, and results in a little over two hours before the socket would
|
||||
// be closed due to keepalive. A shorter default might be better, or seeking
|
||||
// a default from the host IP stack. This also might be a useful
|
||||
// user-tunable, as in userspace mode this can have broad implications such
|
||||
// as lingering connections to fork style daemons. On the other side of the
|
||||
// fence, the long duration timers are low impact values for battery powered
|
||||
// peers.
|
||||
ep.SocketOptions().SetKeepAlive(true)
|
||||
|
||||
// The ForwarderRequest.CreateEndpoint above asynchronously
|
||||
// starts the TCP handshake. Note that the gonet.TCPConn
|
||||
// methods c.RemoteAddr() and c.LocalAddr() will return nil
|
||||
// until the handshake actually completes. But we have the
|
||||
// remote address in reqDetails instead, so we don't use
|
||||
// gonet.TCPConn.RemoteAddr. The byte copies in both
|
||||
// directions to/from the gonet.TCPConn in forwardTCP will
|
||||
// block until the TCP handshake is complete.
|
||||
c := gonet.NewTCPConn(&wq, ep)
|
||||
|
||||
// DNS
|
||||
if reqDetails.LocalPort == 53 && (dialIP == magicDNSIP || dialIP == magicDNSIPv6) {
|
||||
c := createConn()
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
go ns.dns.HandleTCPConn(c, netip.AddrPortFrom(clientRemoteIP, reqDetails.RemotePort))
|
||||
return
|
||||
}
|
||||
|
||||
if ns.lb != nil {
|
||||
if reqDetails.LocalPort == 22 && ns.processSSH() && ns.isLocalIP(dialIP) {
|
||||
c := createConn()
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
if err := ns.lb.HandleSSHConn(c); err != nil {
|
||||
ns.logf("ssh error: %v", err)
|
||||
}
|
||||
@@ -791,6 +808,11 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
}
|
||||
if port, ok := ns.lb.GetPeerAPIPort(dialIP); ok {
|
||||
if reqDetails.LocalPort == port && ns.isLocalIP(dialIP) {
|
||||
c := createConn()
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
src := netip.AddrPortFrom(clientRemoteIP, reqDetails.RemotePort)
|
||||
dst := netip.AddrPortFrom(dialIP, port)
|
||||
ns.lb.ServePeerAPIConnection(src, dst, c)
|
||||
@@ -798,12 +820,20 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
}
|
||||
}
|
||||
if reqDetails.LocalPort == 80 && (dialIP == magicDNSIP || dialIP == magicDNSIPv6) {
|
||||
c := createConn()
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
ns.lb.HandleQuad100Port80Conn(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ns.ForwardTCPIn != nil {
|
||||
c := createConn()
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
ns.ForwardTCPIn(c, reqDetails.LocalPort)
|
||||
return
|
||||
}
|
||||
@@ -811,11 +841,13 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
dialIP = netaddr.IPv4(127, 0, 0, 1)
|
||||
}
|
||||
dialAddr := netip.AddrPortFrom(dialIP, uint16(reqDetails.LocalPort))
|
||||
ns.forwardTCP(c, clientRemoteIP, &wq, dialAddr)
|
||||
|
||||
if !ns.forwardTCP(createConn, clientRemoteIP, &wq, dialAddr) {
|
||||
r.Complete(true) // sends a RST
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *Impl) forwardTCP(client *gonet.TCPConn, clientRemoteIP netip.Addr, wq *waiter.Queue, dialAddr netip.AddrPort) {
|
||||
defer client.Close()
|
||||
func (ns *Impl) forwardTCP(getClient func() *gonet.TCPConn, clientRemoteIP netip.Addr, wq *waiter.Queue, dialAddr netip.AddrPort) (handled bool) {
|
||||
dialAddrStr := dialAddr.String()
|
||||
if debugNetstack {
|
||||
ns.logf("[v2] netstack: forwarding incoming connection to %s", dialAddrStr)
|
||||
@@ -823,6 +855,7 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, clientRemoteIP netip.Addr, wq
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
waitEntry, notifyCh := waiter.NewChannelEntry(waiter.EventHUp) // TODO(bradfitz): right EventMask?
|
||||
wq.EventRegister(&waitEntry)
|
||||
defer wq.EventUnregister(&waitEntry)
|
||||
@@ -840,13 +873,29 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, clientRemoteIP netip.Addr, wq
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Attempt to dial the outbound connection before we accept the inbound one.
|
||||
var stdDialer net.Dialer
|
||||
server, err := stdDialer.DialContext(ctx, "tcp", dialAddrStr)
|
||||
if err != nil {
|
||||
ns.logf("netstack: could not connect to local server at %s: %v", dialAddrStr, err)
|
||||
ns.logf("netstack: could not connect to local server at %s: %v", dialAddr.String(), err)
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
// If we get here, either the getClient call below will succeed and
|
||||
// return something we can Close, or it will fail and will properly
|
||||
// respond to the client with a RST. Either way, the caller no longer
|
||||
// needs to clean up the client connection.
|
||||
handled = true
|
||||
|
||||
// We dialed the connection; we can complete the client's TCP handshake.
|
||||
client := getClient()
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
backendLocalAddr := server.LocalAddr().(*net.TCPAddr)
|
||||
backendLocalIPPort := netaddr.Unmap(backendLocalAddr.AddrPort())
|
||||
ns.e.RegisterIPPortIdentity(backendLocalIPPort, clientRemoteIP)
|
||||
@@ -865,6 +914,7 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, clientRemoteIP netip.Addr, wq
|
||||
ns.logf("proxy connection closed with error: %v", err)
|
||||
}
|
||||
ns.logf("[v2] netstack: forwarder connection to %s closed", dialAddrStr)
|
||||
return
|
||||
}
|
||||
|
||||
func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) {
|
||||
|
||||
@@ -1005,6 +1005,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
closing := e.closing
|
||||
peerKeys := make([]key.NodePublic, len(e.peerSequence))
|
||||
copy(peerKeys, e.peerSequence)
|
||||
localAddrs := append([]tailcfg.Endpoint(nil), e.endpoints...)
|
||||
e.mu.Unlock()
|
||||
|
||||
if closing {
|
||||
@@ -1020,7 +1021,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
|
||||
return &Status{
|
||||
AsOf: time.Now(),
|
||||
LocalAddrs: append([]tailcfg.Endpoint(nil), e.endpoints...),
|
||||
LocalAddrs: localAddrs,
|
||||
Peers: peers,
|
||||
DERPs: derpConns,
|
||||
}, nil
|
||||
|
||||
Reference in New Issue
Block a user