From 17962f5e6568e58284ca433e777b9a8fa51ca4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 28 Mar 2025 12:56:43 +0800 Subject: [PATCH] Add DERP inbound --- common/dialer/dialer.go | 4 +- constant/proxy.go | 1 + go.mod | 6 +- go.sum | 19 +- include/registry.go | 1 + include/tailscale.go | 5 + include/tailscale_stub.go | 7 + option/tailscale.go | 21 ++ protocol/tailscale/derp.go | 487 +++++++++++++++++++++++++++++++++ protocol/tailscale/endpoint.go | 23 +- 10 files changed, 555 insertions(+), 19 deletions(-) create mode 100644 protocol/tailscale/derp.go diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index 88e16740..bfd13784 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -102,12 +102,12 @@ func NewWithOptions(options Options) (N.Dialer, error) { } dnsQueryOptions.Transport = transport resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay) - } else if options.NewDialer { - return nil, E.New("missing domain resolver for domain server address") } else { transports := dnsTransport.Transports() if len(transports) < 2 { dnsQueryOptions.Transport = dnsTransport.Default() + } else if options.NewDialer { + return nil, E.New("missing domain resolver for domain server address") } else { deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver) } diff --git a/constant/proxy.go b/constant/proxy.go index 1044428b..743babbc 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -25,6 +25,7 @@ const ( TypeTUIC = "tuic" TypeHysteria2 = "hysteria2" TypeTailscale = "tailscale" + TypeDERP = "derp" ) const ( diff --git a/go.mod b/go.mod index d5bd0b0c..bf55932a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/anytls/sing-anytls v0.0.7 github.com/caddyserver/certmagic v0.21.7 github.com/cloudflare/circl v1.6.0 + github.com/coder/websocket v1.8.12 github.com/cretz/bine v0.2.0 github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/render v1.0.3 @@ -35,7 +36,7 @@ require ( github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec github.com/sagernet/sing-vmess v0.2.0 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 - github.com/sagernet/tailscale v1.80.3-mod.0 + github.com/sagernet/tailscale v1.80.3-mod.0.0.20250328114533-953541d9750a github.com/sagernet/utls v1.6.7 github.com/sagernet/wireguard-go v0.0.1-beta.5 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 @@ -66,7 +67,6 @@ require ( github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/coder/websocket v1.8.12 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect @@ -98,6 +98,7 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect @@ -136,6 +137,7 @@ require ( golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 1c20cdaf..e1022d4e 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,7 @@ github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3C github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -110,6 +111,13 @@ github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kK github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE= github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054= @@ -145,6 +153,7 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5 github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -154,6 +163,9 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= @@ -196,8 +208,8 @@ github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGT github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= -github.com/sagernet/tailscale v1.80.3-mod.0 h1:oHIdivbR/yxoiA9d3a2rRlhYn2shY9XVF35Rr8jW508= -github.com/sagernet/tailscale v1.80.3-mod.0/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= +github.com/sagernet/tailscale v1.80.3-mod.0.0.20250328114533-953541d9750a h1:LjpPc74+xv8flXbZ4YUsO8vUXaZ6EwWXkaOx7gE9Bg0= +github.com/sagernet/tailscale v1.80.3-mod.0.0.20250328114533-953541d9750a/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8= github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM= github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc= @@ -318,8 +330,9 @@ google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/include/registry.go b/include/registry.go index 9be1f2b4..db110f3d 100644 --- a/include/registry.go +++ b/include/registry.go @@ -58,6 +58,7 @@ func InboundRegistry() *inbound.Registry { registerQUICInbounds(registry) registerStubForRemovedInbounds(registry) + registerTailscaleInbound(registry) return registry } diff --git a/include/tailscale.go b/include/tailscale.go index 05eed2cd..01278efe 100644 --- a/include/tailscale.go +++ b/include/tailscale.go @@ -4,10 +4,15 @@ package include import ( "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/protocol/tailscale" ) +func registerTailscaleInbound(registry *inbound.Registry) { + tailscale.RegisterDERPInbound(registry) +} + func registerTailscaleEndpoint(registry *endpoint.Registry) { tailscale.RegisterEndpoint(registry) } diff --git a/include/tailscale_stub.go b/include/tailscale_stub.go index ddf6485e..d1b9a769 100644 --- a/include/tailscale_stub.go +++ b/include/tailscale_stub.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/adapter/inbound" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/log" @@ -14,6 +15,12 @@ import ( E "github.com/sagernet/sing/common/exceptions" ) +func registerTailscaleInbound(registry *inbound.Registry) { + inbound.Register[option.DERPInboundOptions](registry, C.TypeTailscale, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DERPInboundOptions) (adapter.Inbound, error) { + return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) + }) +} + func registerTailscaleEndpoint(registry *endpoint.Registry) { endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) { return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) diff --git a/option/tailscale.go b/option/tailscale.go index 30579fc7..b564e1a6 100644 --- a/option/tailscale.go +++ b/option/tailscale.go @@ -2,6 +2,8 @@ package option import ( "net/netip" + + "github.com/sagernet/sing/common/json/badoption" ) type TailscaleEndpointOptions struct { @@ -22,3 +24,22 @@ type TailscaleDNSServerOptions struct { Endpoint string `json:"endpoint,omitempty"` AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"` } + +type DERPInboundOptions struct { + ListenOptions + STUNPort uint16 `json:"stun_port,omitempty"` + InboundTLSOptionsContainer + ConfigPath string `json:"config_path,omitempty"` + VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"` + VerifyClientURL badoption.Listable[string] `json:"verify_client_url,omitempty"` + MeshWith []DERPMeshOptions `json:"mesh_with,omitempty"` + MeshPSK string `json:"mesh_psk,omitempty"` + MeshPSKFile string `json:"mesh_psk_file,omitempty"` + DialerOptions +} + +type DERPMeshOptions struct { + ServerOptions + OutboundTLSOptionsContainer + Hostname string `json:"hostname,omitempty"` +} diff --git a/protocol/tailscale/derp.go b/protocol/tailscale/derp.go new file mode 100644 index 00000000..a4365cab --- /dev/null +++ b/protocol/tailscale/derp.go @@ -0,0 +1,487 @@ +package tailscale + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/netip" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + aTLS "github.com/sagernet/sing/common/tls" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/filemanager" + "github.com/sagernet/tailscale/client/tailscale" + "github.com/sagernet/tailscale/derp" + "github.com/sagernet/tailscale/derp/derphttp" + "github.com/sagernet/tailscale/net/netmon" + "github.com/sagernet/tailscale/net/stun" + "github.com/sagernet/tailscale/net/wsconn" + "github.com/sagernet/tailscale/tsweb" + "github.com/sagernet/tailscale/types/key" + + "github.com/coder/websocket" + "github.com/go-chi/render" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +func RegisterDERPInbound(registry *inbound.Registry) { + inbound.Register[option.DERPInboundOptions](registry, C.TypeDERP, NewDERPInbound) +} + +type DERPInbound struct { + inbound.Adapter + ctx context.Context + logger logger.ContextLogger + listener *listener.Listener + stunListener *listener.Listener + dialer N.Dialer + tlsConfig tls.ServerConfig + server *derp.Server + configPath string + verifyClientEndpoint []string + verifyClientURL []string + home string + meshKey string + meshKeyPath string + meshWith []option.DERPMeshOptions +} + +func NewDERPInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DERPInboundOptions) (adapter.Inbound, error) { + outboundDialer, err := dialer.NewWithOptions(dialer.Options{ + Context: ctx, + Options: options.DialerOptions, + RemoteIsDomain: true, + ResolverOnDetour: true, + NewDialer: true, + }) + if err != nil { + return nil, err + } + + stunListenOptions := options.ListenOptions + stunListenOptions.ListenPort = options.STUNPort + + if options.TLS == nil || !options.TLS.Enabled { + return nil, E.New("TLS is required for DERP server") + } + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + + var configPath string + if options.ConfigPath != "" { + configPath = filemanager.BasePath(ctx, os.ExpandEnv(options.ConfigPath)) + } else if os.Getuid() == 0 { + configPath = "/var/lib/derper/derper.key" + } else { + return nil, E.New("missing config_path") + } + + if options.MeshPSK != "" { + err = checkMeshKey(options.MeshPSK) + if err != nil { + return nil, E.Cause(err, "invalid mesh_psk") + } + } + + return &DERPInbound{ + Adapter: inbound.NewAdapter(C.TypeDERP, tag), + ctx: ctx, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + }), + stunListener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: stunListenOptions, + }), + dialer: outboundDialer, + tlsConfig: tlsConfig, + configPath: configPath, + verifyClientEndpoint: options.VerifyClientEndpoint, + verifyClientURL: options.VerifyClientURL, + meshKey: options.MeshPSK, + meshKeyPath: options.MeshPSKFile, + meshWith: options.MeshWith, + }, nil +} + +func (d *DERPInbound) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + config, err := readDERPConfig(d.configPath) + if err != nil { + return err + } + + server := derp.NewServer(config.PrivateKey, func(format string, args ...any) { + d.logger.Debug(fmt.Sprintf(format, args...)) + }) + + if len(d.verifyClientURL) > 0 { + server.SetVerifyClientHTTPClient(&http.Client{ + Transport: &http.Transport{ + ForceAttemptHTTP2: true, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return d.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }, + }, + }) + server.SetVerifyClientURL(d.verifyClientURL) + } + + if d.meshKey != "" { + server.SetMeshKey(d.meshKey) + } else if d.meshKeyPath != "" { + var meshKeyContent []byte + meshKeyContent, err = os.ReadFile(d.meshKeyPath) + if err != nil { + return err + } + err = checkMeshKey(string(meshKeyContent)) + if err != nil { + return E.Cause(err, "invalid mesh_psk_path file") + } + server.SetMeshKey(string(meshKeyContent)) + } + d.server = server + + derpMux := http.NewServeMux() + derpHandler := derphttp.Handler(server) + derpHandler = addWebSocketSupport(server, derpHandler) + derpMux.Handle("/derp", derpHandler) + + homeHandler, ok := getHomeHandler(d.home) + if !ok { + return E.New("invalid home value: ", d.home) + } + + derpMux.HandleFunc("/derp/probe", derphttp.ProbeHandler) + derpMux.HandleFunc("/derp/latency-check", derphttp.ProbeHandler) + derpMux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS(d.ctx, d.dialer.(dialer.ResolveDialer).QueryOptions()))) + derpMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tsweb.AddBrowserHeaders(w) + homeHandler.ServeHTTP(w, r) + })) + derpMux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tsweb.AddBrowserHeaders(w) + io.WriteString(w, "User-agent: *\nDisallow: /\n") + })) + derpMux.Handle("/generate_204", http.HandlerFunc(derphttp.ServeNoContent)) + + err = d.tlsConfig.Start() + if err != nil { + return err + } + + tcpListener, err := d.listener.ListenTCP() + if err != nil { + return err + } + if len(d.tlsConfig.NextProtos()) == 0 { + d.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"}) + } else if !common.Contains(d.tlsConfig.NextProtos(), http2.NextProtoTLS) { + d.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, d.tlsConfig.NextProtos()...)) + } + tcpListener = aTLS.NewListener(tcpListener, d.tlsConfig) + httpServer := &http.Server{ + Handler: h2c.NewHandler(derpMux, &http2.Server{}), + } + go httpServer.Serve(tcpListener) + + if d.stunListener.ListenOptions().ListenPort != 0 { + packetConn, err := d.stunListener.ListenUDP() + if err != nil { + return err + } + go d.loopSTUN(packetConn.(*net.UDPConn)) + } + case adapter.StartStatePostStart: + if len(d.verifyClientEndpoint) > 0 { + var endpoints []*tailscale.LocalClient + endpointManager := service.FromContext[adapter.EndpointManager](d.ctx) + for _, endpointTag := range d.verifyClientEndpoint { + endpoint, loaded := endpointManager.Get(endpointTag) + if !loaded { + return E.New("verify_client_endpoint: endpoint not found: ", endpointTag) + } + tsEndpoint, isTailscale := endpoint.(*Endpoint) + if !isTailscale { + return E.New("verify_client_endpoint: endpoint is not Tailscale: ", endpointTag) + } + localClient, err := tsEndpoint.server.LocalClient() + if err != nil { + return err + } + endpoints = append(endpoints, localClient) + } + d.server.SetVerifyClientLocalClient(endpoints) + } + if len(d.meshWith) > 0 { + if !d.server.HasMeshKey() { + return E.New("missing mesh psk") + } + for _, options := range d.meshWith { + err := d.startMeshWithHost(d.server, options) + if err != nil { + return err + } + } + } + } + return nil +} + +func checkMeshKey(meshKey string) error { + checkRegex, err := regexp.Compile(`^[0-9a-f]{64}$`) + if err != nil { + return err + } + if !checkRegex.MatchString(meshKey) { + return E.New("key must contain exactly 64 hex digits") + } + return nil +} + +func (d *DERPInbound) startMeshWithHost(derpServer *derp.Server, server option.DERPMeshOptions) error { + var hostname string + if server.Hostname != "" { + hostname = server.Hostname + } else { + hostname = server.Server + } + var stdConfig *tls.STDConfig + if server.TLS != nil && server.TLS.Enabled { + tlsConfig, err := tls.NewClient(d.ctx, hostname, common.PtrValueOrDefault(server.TLS)) + if err != nil { + return err + } + stdConfig, err = tlsConfig.Config() + if err != nil { + return err + } + } + logf := func(format string, args ...any) { + d.logger.Debug(F.ToString("mesh(", hostname, "): ", fmt.Sprintf(format, args...))) + } + meshClient, err := derphttp.NewClient(derpServer.PrivateKey(), "https://"+server.Build().String()+"/derp", logf, netmon.NewStatic()) + if err != nil { + return err + } + meshClient.TLSConfig = stdConfig + meshClient.MeshKey = derpServer.MeshKey() + meshClient.WatchConnectionChanges = true + meshClient.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) { + return d.dialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) + }) + add := func(m derp.PeerPresentMessage) { derpServer.AddPacketForwarder(m.Key, meshClient) } + remove := func(m derp.PeerGoneMessage) { derpServer.RemovePacketForwarder(m.Peer, meshClient) } + go meshClient.RunWatchConnectionLoop(context.Background(), derpServer.PublicKey(), logf, add, remove) + return nil +} + +func (d *DERPInbound) Close() error { + return common.Close( + common.PtrOrNil(d.listener), + common.PtrOrNil(d.stunListener), + d.tlsConfig, + ) +} + +var homePage = ` +

DERP

+

+ This is a Tailscale DERP server. +

+ +

+ It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic + for Tailscale clients. +

+ +

+ Documentation: +

+ +