tsnet: add ListenFunnel
This lets a tsnet binary share a server out over Tailscale Funnel. Signed-off-by: David Crawshaw <crawshaw@tailscale.com> Signed-off-by: Maisem Ali <maisem@tailscale.com> Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
This commit is contained in:
parent
047b324933
commit
ccdd534e81
|
@ -21,10 +21,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
@ -679,7 +677,7 @@ func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
if err := checkHasAccess(st.Self.Capabilities); err != nil {
|
||||
if err := ipn.CheckFunnelAccess(st.Self.Capabilities); err != nil {
|
||||
return err
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
|
@ -702,22 +700,3 @@ func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkHasAccess checks three things: 1) an invite was used to join the
|
||||
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
|
||||
// If any of these are false, an error is returned describing the problem.
|
||||
//
|
||||
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
|
||||
// the attribute we're checking for and possibly warning-capabilities for Funnel.
|
||||
func checkHasAccess(nodeAttrs []string) error {
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
|
||||
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
|
||||
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/kb/1153/enabling-https/.")
|
||||
}
|
||||
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
|
||||
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -48,30 +48,6 @@ func TestCleanMountPoint(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCheckHasAccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
caps []string
|
||||
wantErr bool
|
||||
}{
|
||||
{[]string{}, true}, // No "funnel" attribute
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
|
||||
{[]string{tailcfg.NodeAttrFunnel}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := checkHasAccess(tt.caps)
|
||||
switch {
|
||||
case err != nil && tt.wantErr,
|
||||
err == nil && !tt.wantErr:
|
||||
continue
|
||||
case tt.wantErr:
|
||||
t.Fatalf("got no error, want error")
|
||||
case !tt.wantErr:
|
||||
t.Fatalf("got error %v, want no error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeConfigMutations(t *testing.T) {
|
||||
// Stateful mutations, starting from an empty config.
|
||||
type step struct {
|
||||
|
|
23
ipn/serve.go
23
ipn/serve.go
|
@ -4,8 +4,12 @@
|
|||
package ipn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// ServeConfigKey returns a StateKey that stores the
|
||||
|
@ -168,3 +172,22 @@ func (sc *ServeConfig) IsFunnelOn() bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckFunnelAccess checks three things: 1) an invite was used to join the
|
||||
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
|
||||
// If any of these are false, an error is returned describing the problem.
|
||||
//
|
||||
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
|
||||
// the attribute we're checking for and possibly warning-capabilities for Funnel.
|
||||
func CheckFunnelAccess(nodeAttrs []string) error {
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
|
||||
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
|
||||
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/kb/1153/enabling-https/.")
|
||||
}
|
||||
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
|
||||
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestCheckFunnelAccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
caps []string
|
||||
wantErr bool
|
||||
}{
|
||||
{[]string{}, true}, // No "funnel" attribute
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
|
||||
{[]string{tailcfg.NodeAttrFunnel}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := CheckFunnelAccess(tt.caps)
|
||||
switch {
|
||||
case err != nil && tt.wantErr,
|
||||
err == nil && !tt.wantErr:
|
||||
continue
|
||||
case tt.wantErr:
|
||||
t.Fatalf("got no error, want error")
|
||||
case !tt.wantErr:
|
||||
t.Fatalf("got error %v, want no error", err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,130 +2,40 @@
|
|||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The tsnet-funnel server demonstrates how to use tsnet with Funnel.
|
||||
//
|
||||
// To use it, generate an auth key from the Tailscale admin panel and
|
||||
// run the demo with the key:
|
||||
//
|
||||
// TS_AUTHKEY=<yourkey> go run tsnet-funnel.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":443", "address to listen on")
|
||||
)
|
||||
|
||||
func enableFunnel(ctx context.Context, s *tsnet.Server) error {
|
||||
st, err := s.Up(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(st.CertDomains) == 0 {
|
||||
return errors.New("tsnet: you must enable HTTPS in the admin panel to proceed")
|
||||
}
|
||||
domain := st.CertDomains[0]
|
||||
|
||||
hp := ipn.HostPort(net.JoinHostPort(domain, "443"))
|
||||
|
||||
srvConfig := &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{
|
||||
hp: true,
|
||||
},
|
||||
}
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return lc.SetServeConfig(ctx, srvConfig)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
s := new(tsnet.Server)
|
||||
defer s.Close()
|
||||
ctx := context.Background()
|
||||
if err := enableFunnel(ctx, s); err != nil {
|
||||
log.Fatal(err)
|
||||
s := &tsnet.Server{
|
||||
Dir: "./funnel-demo-config.state",
|
||||
Logf: logger.Discard,
|
||||
Hostname: "fun",
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ln, err := s.Listen("tcp", *addr)
|
||||
ln, err := s.ListenFunnel("tcp", ":443")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ln = tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: lc.GetCertificate,
|
||||
})
|
||||
httpServer := &http.Server{
|
||||
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
|
||||
if tc, ok := c.(*tls.Conn); ok {
|
||||
// First unwrap the TLS connection to get the underlying
|
||||
// net.Conn.
|
||||
c = tc.NetConn()
|
||||
}
|
||||
// Then check if the underlying net.Conn is a FunnelConn.
|
||||
if fc, ok := c.(*ipn.FunnelConn); ok {
|
||||
ctx = context.WithValue(ctx, funnelKey{}, true)
|
||||
ctx = context.WithValue(ctx, funnelSrcKey{}, fc.Src)
|
||||
}
|
||||
return ctx
|
||||
},
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if isFunnel(r.Context()) {
|
||||
fmt.Fprintln(w, "<html><body><h1>Hello, internet!</h1>")
|
||||
fmt.Fprintln(w, "<p>You are connected over the internet!</p>")
|
||||
fmt.Fprintf(w, "<p>You are coming from %v</p></html>\n", funnelSrc(r.Context()))
|
||||
} else {
|
||||
fmt.Fprintln(w, "<html><body><h1>Hello, tailnet!</h1>")
|
||||
fmt.Fprintln(w, "<p>You are connected over the tailnet!</p>")
|
||||
who, err := lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
if err != nil {
|
||||
log.Printf("WhoIs(%v): %v", r.RemoteAddr, err)
|
||||
fmt.Fprintf(w, "<p>I do not know who you are</p>")
|
||||
} else if len(who.Node.Tags) > 0 {
|
||||
fmt.Fprintf(w, "<p>You are using a tagged node: %v</p>\n", who.Node.Tags)
|
||||
} else {
|
||||
fmt.Fprintf(w, "<p>You are %v</p>\n", who.UserProfile.DisplayName)
|
||||
}
|
||||
fmt.Fprintf(w, "<p>You are coming from %v</p></html>\n", r.RemoteAddr)
|
||||
}
|
||||
}),
|
||||
}
|
||||
log.Fatal(httpServer.Serve(ln))
|
||||
}
|
||||
|
||||
// funnelKey is a context key used to indicate that a request is coming
|
||||
// over the internet.
|
||||
// It is not used by tsnet, but is used by this example to demonstrate
|
||||
// how to detect when a request is coming over the internet rather than
|
||||
// over the tailnet.
|
||||
type funnelKey struct{}
|
||||
|
||||
// funnelSrcKey is a context key used to indicate the source of a
|
||||
// request.
|
||||
type funnelSrcKey struct{}
|
||||
|
||||
// isFunnel reports whether the request is coming over the internet.
|
||||
func isFunnel(ctx context.Context) bool {
|
||||
v, _ := ctx.Value(funnelKey{}).(bool)
|
||||
return v
|
||||
}
|
||||
|
||||
func funnelSrc(ctx context.Context) netip.AddrPort {
|
||||
v, _ := ctx.Value(funnelSrcKey{}).(netip.AddrPort)
|
||||
return v
|
||||
err = http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "<html><body><h1>Hello, internet!</h1>")
|
||||
}))
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
189
tsnet/tsnet.go
189
tsnet/tsnet.go
|
@ -9,6 +9,7 @@ package tsnet
|
|||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -415,9 +416,9 @@ func (s *Server) start() (reterr error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(s.rootPath, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(s.rootPath, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if fi, err := os.Stat(s.rootPath); err != nil {
|
||||
return err
|
||||
|
@ -645,7 +646,7 @@ func networkForFamily(netBase string, is6 bool) string {
|
|||
// - ("tcp", "", port)
|
||||
//
|
||||
// The netBase is "tcp" or "udp" (without any '4' or '6' suffix).
|
||||
func (s *Server) listenerForDstAddr(netBase string, dst netip.AddrPort) (_ *listener, ok bool) {
|
||||
func (s *Server) listenerForDstAddr(netBase string, dst netip.AddrPort, funnel bool) (_ *listener, ok bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, a := range [2]netip.Addr{0: dst.Addr()} {
|
||||
|
@ -653,7 +654,7 @@ func (s *Server) listenerForDstAddr(netBase string, dst netip.AddrPort) (_ *list
|
|||
networkForFamily(netBase, dst.Addr().Is6()),
|
||||
netBase,
|
||||
} {
|
||||
if ln, ok := s.listeners[listenKey{net, a, dst.Port()}]; ok {
|
||||
if ln, ok := s.listeners[listenKey{net, a, dst.Port(), funnel}]; ok {
|
||||
return ln, true
|
||||
}
|
||||
}
|
||||
|
@ -675,7 +676,7 @@ func (s *Server) getTCPHandlerForFunnelFlow(src netip.AddrPort, dstPort uint16)
|
|||
}
|
||||
dst = netip.AddrPortFrom(ipv6, dstPort)
|
||||
}
|
||||
ln, ok := s.listenerForDstAddr("tcp", dst)
|
||||
ln, ok := s.listenerForDstAddr("tcp", dst, true)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
@ -683,7 +684,7 @@ func (s *Server) getTCPHandlerForFunnelFlow(src netip.AddrPort, dstPort uint16)
|
|||
}
|
||||
|
||||
func (s *Server) getTCPHandlerForFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
|
||||
ln, ok := s.listenerForDstAddr("tcp", dst)
|
||||
ln, ok := s.listenerForDstAddr("tcp", dst, false)
|
||||
if !ok {
|
||||
return nil, true // don't handle, don't forward to localhost
|
||||
}
|
||||
|
@ -691,7 +692,7 @@ func (s *Server) getTCPHandlerForFlow(src, dst netip.AddrPort) (handler func(net
|
|||
}
|
||||
|
||||
func (s *Server) getUDPHandlerForFlow(src, dst netip.AddrPort) (handler func(nettype.ConnPacketConn), intercept bool) {
|
||||
ln, ok := s.listenerForDstAddr("udp", dst)
|
||||
ln, ok := s.listenerForDstAddr("udp", dst, false)
|
||||
if !ok {
|
||||
return nil, true // don't handle, don't forward to localhost
|
||||
}
|
||||
|
@ -760,6 +761,136 @@ func (s *Server) APIClient() (*tailscale.Client, error) {
|
|||
// Listen announces only on the Tailscale network.
|
||||
// It will start the server if it has not been started yet.
|
||||
func (s *Server) Listen(network, addr string) (net.Listener, error) {
|
||||
return s.listen(network, addr, listenOnTailnet)
|
||||
}
|
||||
|
||||
// ListenTLS announces only on the Tailscale network.
|
||||
// It returns a TLS listener wrapping the tsnet listener.
|
||||
// It will start the server if it has not been started yet.
|
||||
func (s *Server) ListenTLS(network string, addr string) (net.Listener, error) {
|
||||
if network != "tcp" {
|
||||
return nil, fmt.Errorf("ListenTLS(%q, %q): only tcp is supported", network, addr)
|
||||
}
|
||||
ctx := context.Background()
|
||||
st, err := s.Up(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(st.CertDomains) == 0 {
|
||||
return nil, errors.New("tsnet: you must enable HTTPS in the admin panel to proceed")
|
||||
}
|
||||
|
||||
lc, err := s.LocalClient() // do local client first before listening.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ln, err := s.listen(network, addr, listenOnTailnet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: lc.GetCertificate,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// FunnelOption is an option passed to ListenFunnel to configure the listener.
|
||||
type FunnelOption interface {
|
||||
funnelOption()
|
||||
}
|
||||
|
||||
type funnelOnly int
|
||||
|
||||
func (funnelOnly) funnelOption() {}
|
||||
|
||||
// FunnelOnly configures the listener to only respond to connections from Tailscale Funnel.
|
||||
// The local tailnet will not be able to connect to the listener.
|
||||
func FunnelOnly() FunnelOption { return funnelOnly(1) }
|
||||
|
||||
// ListenFunnel announces on the public internet using Tailscale Funnel.
|
||||
//
|
||||
// It also by default listens on your local tailnet, so connections can
|
||||
// come from either inside or outside your network. To restrict connections
|
||||
// to be just from the internet, use the FunnelOnly option.
|
||||
//
|
||||
// Currently (2023-03-10), Funnel only supports TCP on ports 443, 8443, and 10000.
|
||||
// The supported host name is limited to that configured for the tsnet.Server.
|
||||
// As such, the standard way to create funnel is:
|
||||
//
|
||||
// s.ListenFunnel("tcp", ":443")
|
||||
//
|
||||
// and the only other supported addrs currently are ":8443" and ":10000".
|
||||
//
|
||||
// It will start the server if it has not been started yet.
|
||||
func (s *Server) ListenFunnel(network string, addr string, opts ...FunnelOption) (net.Listener, error) {
|
||||
if network != "tcp" {
|
||||
return nil, fmt.Errorf("ListenFunnel(%q, %q): only tcp is supported", network, addr)
|
||||
}
|
||||
switch addr {
|
||||
case ":443", ":8443", ":10000":
|
||||
default:
|
||||
return nil, fmt.Errorf(`ListenFunnel(%q, %q): only valid addrs are ":443", ":8443", and ":10000"`, network, addr)
|
||||
}
|
||||
ctx := context.Background()
|
||||
st, err := s.Up(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(st.CertDomains) == 0 {
|
||||
return nil, errors.New("tsnet: you must enable HTTPS in the admin panel to proceed")
|
||||
}
|
||||
if err := ipn.CheckFunnelAccess(st.Self.Capabilities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lc, err := s.LocalClient() // do local client first before listening.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// May not have funnel enabled. Enable it.
|
||||
srvConfig, err := lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if srvConfig == nil {
|
||||
srvConfig = &ipn.ServeConfig{}
|
||||
}
|
||||
domain := st.CertDomains[0]
|
||||
hp := ipn.HostPort(domain + addr) // valid only because of the strong restrictions on addr above
|
||||
if !srvConfig.AllowFunnel[hp] {
|
||||
mak.Set(&srvConfig.AllowFunnel, hp, true)
|
||||
srvConfig.AllowFunnel[hp] = true
|
||||
if err := lc.SetServeConfig(ctx, srvConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Start a funnel listener.
|
||||
lnOn := listenOnBoth
|
||||
for _, opt := range opts {
|
||||
if _, ok := opt.(funnelOnly); ok {
|
||||
lnOn = listenOnFunnel
|
||||
}
|
||||
}
|
||||
ln, err := s.listen(network, addr, lnOn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.NewListener(ln, &tls.Config{
|
||||
GetCertificate: lc.GetCertificate,
|
||||
}), nil
|
||||
}
|
||||
|
||||
type listenOn string
|
||||
|
||||
const (
|
||||
listenOnTailnet = listenOn("listen-on-tailnet")
|
||||
listenOnFunnel = listenOn("listen-on-funnel")
|
||||
listenOnBoth = listenOn("listen-on-both")
|
||||
)
|
||||
|
||||
func (s *Server) listen(network, addr string, lnOn listenOn) (net.Listener, error) {
|
||||
switch network {
|
||||
case "", "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6":
|
||||
default:
|
||||
|
@ -794,20 +925,37 @@ func (s *Server) Listen(network, addr string) (net.Listener, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
key := listenKey{network, bindHostOrZero, uint16(port)}
|
||||
var keys []listenKey
|
||||
switch lnOn {
|
||||
case listenOnTailnet:
|
||||
keys = append(keys, listenKey{network, bindHostOrZero, uint16(port), false})
|
||||
case listenOnFunnel:
|
||||
keys = append(keys, listenKey{network, bindHostOrZero, uint16(port), true})
|
||||
case listenOnBoth:
|
||||
keys = append(keys, listenKey{network, bindHostOrZero, uint16(port), false})
|
||||
keys = append(keys, listenKey{network, bindHostOrZero, uint16(port), true})
|
||||
}
|
||||
|
||||
ln := &listener{
|
||||
s: s,
|
||||
key: key,
|
||||
keys: keys,
|
||||
addr: addr,
|
||||
|
||||
conn: make(chan net.Conn),
|
||||
}
|
||||
s.mu.Lock()
|
||||
if _, ok := s.listeners[key]; ok {
|
||||
s.mu.Unlock()
|
||||
return nil, fmt.Errorf("tsnet: listener already open for %s, %s", network, addr)
|
||||
for _, key := range keys {
|
||||
if _, ok := s.listeners[key]; ok {
|
||||
s.mu.Unlock()
|
||||
return nil, fmt.Errorf("tsnet: listener already open for %s, %s", network, addr)
|
||||
}
|
||||
}
|
||||
if s.listeners == nil {
|
||||
s.listeners = make(map[listenKey]*listener)
|
||||
}
|
||||
for _, key := range keys {
|
||||
s.listeners[key] = ln
|
||||
}
|
||||
mak.Set(&s.listeners, key, ln)
|
||||
s.mu.Unlock()
|
||||
return ln, nil
|
||||
}
|
||||
|
@ -816,11 +964,12 @@ type listenKey struct {
|
|||
network string
|
||||
host netip.Addr // or zero value for unspecified
|
||||
port uint16
|
||||
funnel bool
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
s *Server
|
||||
key listenKey
|
||||
keys []listenKey
|
||||
addr string
|
||||
conn chan net.Conn
|
||||
}
|
||||
|
@ -837,10 +986,12 @@ func (ln *listener) Addr() net.Addr { return addr{ln} }
|
|||
func (ln *listener) Close() error {
|
||||
ln.s.mu.Lock()
|
||||
defer ln.s.mu.Unlock()
|
||||
if v, ok := ln.s.listeners[ln.key]; ok && v == ln {
|
||||
delete(ln.s.listeners, ln.key)
|
||||
close(ln.conn)
|
||||
for _, key := range ln.keys {
|
||||
if v, ok := ln.s.listeners[key]; ok && v == ln {
|
||||
delete(ln.s.listeners, key)
|
||||
}
|
||||
}
|
||||
close(ln.conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -861,5 +1012,5 @@ func (ln *listener) Server() *Server { return ln.s }
|
|||
|
||||
type addr struct{ ln *listener }
|
||||
|
||||
func (a addr) Network() string { return a.ln.key.network }
|
||||
func (a addr) Network() string { return a.ln.keys[0].network }
|
||||
func (a addr) String() string { return a.ln.addr }
|
||||
|
|
Loading…
Reference in New Issue