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: +
+ +