From 4a162455c7ed8fc8a9024126a0ea2dbcc0d433ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=96=E7=95=8C?=
Date: Sat, 29 Mar 2025 17:24:34 +0800
Subject: [PATCH] Add DERP service
---
constant/proxy.go | 1 +
docs/configuration/endpoint/index.md | 2 +-
docs/configuration/index.md | 2 +
docs/configuration/index.zh.md | 2 +
docs/configuration/service/derp.md | 130 +++++++
docs/configuration/service/index.md | 30 ++
go.mod | 2 +-
include/registry.go | 4 +
include/tailscale.go | 6 +
include/tailscale_stub.go | 7 +
mkdocs.yml | 3 +
option/tailscale.go | 90 +++++
service/derp/service.go | 518 +++++++++++++++++++++++++++
test/box_test.go | 2 +-
14 files changed, 796 insertions(+), 3 deletions(-)
create mode 100644 docs/configuration/service/derp.md
create mode 100644 docs/configuration/service/index.md
create mode 100644 service/derp/service.go
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/docs/configuration/endpoint/index.md b/docs/configuration/endpoint/index.md
index 5e98c4cc..59101e75 100644
--- a/docs/configuration/endpoint/index.md
+++ b/docs/configuration/endpoint/index.md
@@ -6,7 +6,7 @@ icon: material/new-box
# Endpoint
-Endpoint is protocols that has both inbound and outbound behavior.
+An endpoint is a protocol with inbound and outbound behavior.
### Structure
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index dc5d0637..1f6eec13 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -14,6 +14,7 @@ sing-box uses JSON for configuration files.
"inbounds": [],
"outbounds": [],
"route": {},
+ "services": [],
"experimental": {}
}
```
@@ -30,6 +31,7 @@ sing-box uses JSON for configuration files.
| `inbounds` | [Inbound](./inbound/) |
| `outbounds` | [Outbound](./outbound/) |
| `route` | [Route](./route/) |
+| `services` | [Service](./service/) |
| `experimental` | [Experimental](./experimental/) |
### Check
diff --git a/docs/configuration/index.zh.md b/docs/configuration/index.zh.md
index a18cefd8..3bdc3521 100644
--- a/docs/configuration/index.zh.md
+++ b/docs/configuration/index.zh.md
@@ -14,6 +14,7 @@ sing-box 使用 JSON 作为配置文件格式。
"inbounds": [],
"outbounds": [],
"route": {},
+ "services": [],
"experimental": {}
}
```
@@ -30,6 +31,7 @@ sing-box 使用 JSON 作为配置文件格式。
| `inbounds` | [入站](./inbound/) |
| `outbounds` | [出站](./outbound/) |
| `route` | [路由](./route/) |
+| `services` | [服务](./service/) |
| `experimental` | [实验性](./experimental/) |
### 检查
diff --git a/docs/configuration/service/derp.md b/docs/configuration/service/derp.md
new file mode 100644
index 00000000..adf80d82
--- /dev/null
+++ b/docs/configuration/service/derp.md
@@ -0,0 +1,130 @@
+---
+icon: material/new-box
+---
+
+!!! question "Since sing-box 1.12.0"
+
+# DERP
+
+DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper).
+
+### Structure
+
+```json
+{
+ "type": "derp",
+
+ ... // Listen Fields
+
+ "tls": {},
+ "config_path": "",
+ "verify_client_endpoint": [],
+ "verify_client_url": [],
+ "mesh_with": [],
+ "mesh_psk": "",
+ "mesh_psk_file": "",
+ "stun": {}
+}
+```
+
+### Listen Fields
+
+See [Listen Fields](/configuration/shared/listen/) for details.
+
+### Fields
+
+#### tls
+
+TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
+
+#### config_path
+
+==Required==
+
+Derper configuration file path.
+
+Example: `derper.key`
+
+#### verify_client_endpoint
+
+Tailscale endpoints tags to verify clients.
+
+#### verify_client_url
+
+URL to verify clients.
+
+Object format:
+
+```json
+{
+ "url": "https://my-headscale.com/verify",
+
+ ... // Dial Fields
+}
+```
+
+Setting Array value to a string `__URL__` is equivalent to configuring:
+
+```json
+{ "url": __URL__ }
+```
+
+#### mesh_with
+
+Mesh with other DERP servers.
+
+Object format:
+
+```json
+{
+ "server": "",
+ "server_port": "",
+ "host": "",
+ "tls": {},
+
+ ... // Dial Fields
+}
+```
+
+Object fields:
+
+- `server`: **Required** DERP server address.
+- `server_port`: **Required** DERP server port.
+- `host`: Custom DERP hostname.
+- `tls`: [TLS](/configuration/shared/tls/#outbound)
+- `Dial Fields`: [Dial Fields](/configuration/shared/dial/)
+
+#### mesh_psk
+
+Pre-shared key for DERP mesh.
+
+#### mesh_psk_file
+
+Pre-shared key file for DERP mesh.
+
+#### stun
+
+STUN server listen options.
+
+Object format:
+
+```json
+{
+ "enabled": true,
+
+ ... // Listen Fields
+}
+```
+
+Object fields:
+
+- `enabled`: **Required** Enable STUN server.
+- `listen`: **Required** STUN server listen address, default to `::`.
+- `listen_port`: **Required** STUN server listen port, default to `3478`.
+- `other Listen Fields`: [Listen Fields](/configuration/shared/listen/)
+
+Setting `stun` value to a number `__PORT__` is equivalent to configuring:
+
+```json
+{ "enabled": true, "listen_port": __PORT__ }
+```
diff --git a/docs/configuration/service/index.md b/docs/configuration/service/index.md
new file mode 100644
index 00000000..7fc64a9f
--- /dev/null
+++ b/docs/configuration/service/index.md
@@ -0,0 +1,30 @@
+---
+icon: material/new-box
+---
+
+!!! question "Since sing-box 1.12.0"
+
+# Service
+
+### Structure
+
+```json
+{
+ "endpoints": [
+ {
+ "type": "",
+ "tag": ""
+ }
+ ]
+}
+```
+
+### Fields
+
+| Type | Format |
+|-------------|--------------------------|
+| `derp` | [DERP](./derp) |
+
+#### tag
+
+The tag of the endpoint.
diff --git a/go.mod b/go.mod
index 6ca08553..3e0bbb92 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
github.com/anytls/sing-anytls v0.0.8
github.com/caddyserver/certmagic v0.23.0
github.com/cloudflare/circl v1.6.1
+ 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
@@ -65,7 +66,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
diff --git a/include/registry.go b/include/registry.go
index a326e586..65e6db7b 100644
--- a/include/registry.go
+++ b/include/registry.go
@@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
+ "github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
@@ -120,6 +121,9 @@ func DNSTransportRegistry() *dns.TransportRegistry {
func ServiceRegistry() *service.Registry {
registry := service.NewRegistry()
+
+ registerDERPService(registry)
+
return registry
}
diff --git a/include/tailscale.go b/include/tailscale.go
index 05eed2cd..1757283b 100644
--- a/include/tailscale.go
+++ b/include/tailscale.go
@@ -4,8 +4,10 @@ package include
import (
"github.com/sagernet/sing-box/adapter/endpoint"
+ "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/protocol/tailscale"
+ "github.com/sagernet/sing-box/service/derp"
)
func registerTailscaleEndpoint(registry *endpoint.Registry) {
@@ -15,3 +17,7 @@ func registerTailscaleEndpoint(registry *endpoint.Registry) {
func registerTailscaleTransport(registry *dns.TransportRegistry) {
tailscale.RegistryTransport(registry)
}
+
+func registerDERPService(registry *service.Registry) {
+ derp.Register(registry)
+}
diff --git a/include/tailscale_stub.go b/include/tailscale_stub.go
index ddf6485e..78398875 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/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
@@ -25,3 +26,9 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) {
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
})
}
+
+func registerDERPService(registry *service.Registry) {
+ service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
+ return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`)
+ })
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index 8ee45c94..a320809d 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -169,6 +169,9 @@ nav:
- DNS: configuration/outbound/dns.md
- Selector: configuration/outbound/selector.md
- URLTest: configuration/outbound/urltest.md
+ - Service:
+ - configuration/service/index.md
+ - DERP: configuration/service/derp.md
markdown_extensions:
- pymdownx.inlinehilite
- pymdownx.snippets
diff --git a/option/tailscale.go b/option/tailscale.go
index 7b901770..74917fcd 100644
--- a/option/tailscale.go
+++ b/option/tailscale.go
@@ -2,6 +2,12 @@ package option
import (
"net/netip"
+ "net/url"
+ "reflect"
+
+ "github.com/sagernet/sing/common/json"
+ "github.com/sagernet/sing/common/json/badoption"
+ M "github.com/sagernet/sing/common/metadata"
)
type TailscaleEndpointOptions struct {
@@ -23,3 +29,87 @@ type TailscaleDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
}
+
+type DERPServiceOptions struct {
+ ListenOptions
+ InboundTLSOptionsContainer
+ ConfigPath string `json:"config_path,omitempty"`
+ VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"`
+ VerifyClientURL badoption.Listable[*DERPVerifyClientURLOptions] `json:"verify_client_url,omitempty"`
+ MeshWith badoption.Listable[*DERPMeshOptions] `json:"mesh_with,omitempty"`
+ MeshPSK string `json:"mesh_psk,omitempty"`
+ MeshPSKFile string `json:"mesh_psk_file,omitempty"`
+ STUN *DERPSTUNListenOptions `json:"stun,omitempty"`
+}
+
+type _DERPVerifyClientURLOptions struct {
+ URL string `json:"url,omitempty"`
+ DialerOptions
+}
+
+type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions
+
+func (d DERPVerifyClientURLOptions) ServerIsDomain() bool {
+ verifyURL, err := url.Parse(d.URL)
+ if err != nil {
+ return false
+ }
+ return M.IsDomainName(verifyURL.Host)
+}
+
+func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) {
+ if reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) {
+ return json.Marshal(d.URL)
+ } else {
+ return json.Marshal(_DERPVerifyClientURLOptions(d))
+ }
+}
+
+func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error {
+ var stringValue string
+ err := json.Unmarshal(bytes, &stringValue)
+ if err == nil {
+ d.URL = stringValue
+ return nil
+ }
+ return json.Unmarshal(bytes, (*_DERPVerifyClientURLOptions)(d))
+}
+
+type DERPMeshOptions struct {
+ ServerOptions
+ Host string `json:"host,omitempty"`
+ OutboundTLSOptionsContainer
+ DialerOptions
+}
+
+type _DERPSTUNListenOptions struct {
+ Enabled bool
+ ListenOptions
+}
+
+type DERPSTUNListenOptions _DERPSTUNListenOptions
+
+func (d DERPSTUNListenOptions) MarshalJSON() ([]byte, error) {
+ portOptions := _DERPSTUNListenOptions{
+ Enabled: d.Enabled,
+ ListenOptions: ListenOptions{
+ ListenPort: d.ListenPort,
+ },
+ }
+ if _DERPSTUNListenOptions(d) == portOptions {
+ return json.Marshal(d.Enabled)
+ } else {
+ return json.Marshal(_DERPSTUNListenOptions(d))
+ }
+}
+
+func (d *DERPSTUNListenOptions) UnmarshalJSON(bytes []byte) error {
+ var portValue uint16
+ err := json.Unmarshal(bytes, &portValue)
+ if err == nil {
+ d.Enabled = true
+ d.ListenPort = portValue
+ return nil
+ }
+ return json.Unmarshal(bytes, (*_DERPSTUNListenOptions)(d))
+}
diff --git a/service/derp/service.go b/service/derp/service.go
new file mode 100644
index 00000000..f950a813
--- /dev/null
+++ b/service/derp/service.go
@@ -0,0 +1,518 @@
+package derp
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/netip"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/sagernet/sing-box/adapter"
+ boxService "github.com/sagernet/sing-box/adapter/service"
+ "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"
+ boxScale "github.com/sagernet/sing-box/protocol/tailscale"
+ "github.com/sagernet/sing/common"
+ E "github.com/sagernet/sing/common/exceptions"
+ F "github.com/sagernet/sing/common/format"
+ "github.com/sagernet/sing/common/json/badoption"
+ "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 Register(registry *boxService.Registry) {
+ boxService.Register[option.DERPServiceOptions](registry, C.TypeDERP, NewService)
+}
+
+type Service struct {
+ boxService.Adapter
+ ctx context.Context
+ logger logger.ContextLogger
+ listener *listener.Listener
+ stunListener *listener.Listener
+ tlsConfig tls.ServerConfig
+ server *derp.Server
+ configPath string
+ verifyClientEndpoint []string
+ verifyClientURL []*option.DERPVerifyClientURLOptions
+ home string
+ meshKey string
+ meshKeyPath string
+ meshWith []*option.DERPMeshOptions
+}
+
+func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
+ 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 {
+ 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")
+ }
+ }
+
+ var stunListener *listener.Listener
+ if options.STUN != nil && options.STUN.Enabled {
+ if options.STUN.Listen == nil {
+ options.STUN.Listen = (*badoption.Addr)(common.Ptr(netip.IPv6Unspecified()))
+ }
+ if options.STUN.ListenPort == 0 {
+ options.STUN.ListenPort = 3478
+ }
+ stunListener = listener.New(listener.Options{
+ Context: ctx,
+ Logger: logger,
+ Network: []string{N.NetworkUDP},
+ Listen: options.STUN.ListenOptions,
+ })
+ }
+
+ return &Service{
+ Adapter: boxService.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: stunListener,
+ tlsConfig: tlsConfig,
+ configPath: configPath,
+ verifyClientEndpoint: options.VerifyClientEndpoint,
+ verifyClientURL: options.VerifyClientURL,
+ meshKey: options.MeshPSK,
+ meshKeyPath: options.MeshPSKFile,
+ meshWith: options.MeshWith,
+ }, nil
+}
+
+func (d *Service) 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 {
+ var httpClients []*http.Client
+ var urls []string
+ for index, options := range d.verifyClientURL {
+ verifyDialer, createErr := dialer.NewWithOptions(dialer.Options{
+ Context: d.ctx,
+ Options: options.DialerOptions,
+ RemoteIsDomain: options.ServerIsDomain(),
+ NewDialer: true,
+ })
+ if createErr != nil {
+ return E.Cause(createErr, "verify_client_url[", index, "]")
+ }
+ httpClients = append(httpClients, &http.Client{
+ Transport: &http.Transport{
+ ForceAttemptHTTP2: true,
+ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
+ },
+ },
+ })
+ urls = append(urls, options.URL)
+ }
+ server.SetVerifyClientHTTPClient(httpClients)
+ server.SetVerifyClientURL(urls)
+ }
+
+ 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)))
+ 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 != nil {
+ stunConn, err := d.stunListener.ListenUDP()
+ if err != nil {
+ return err
+ }
+ go d.loopSTUNPacket(stunConn.(*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.(*boxScale.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 *Service) startMeshWithHost(derpServer *derp.Server, server *option.DERPMeshOptions) error {
+ meshDialer, err := dialer.NewWithOptions(dialer.Options{
+ Context: d.ctx,
+ Options: server.DialerOptions,
+ RemoteIsDomain: server.ServerIsDomain(),
+ NewDialer: true,
+ })
+ if err != nil {
+ return err
+ }
+ var hostname string
+ if server.Host != "" {
+ hostname = server.Host
+ } 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...)))
+ }
+ var meshHost string
+ if server.ServerPort == 0 || server.ServerPort == 443 {
+ meshHost = hostname
+ } else {
+ meshHost = M.ParseSocksaddrHostPort(hostname, server.ServerPort).String()
+ }
+ var serverURL string
+ if stdConfig != nil {
+ serverURL = "https://" + meshHost + "/derp"
+ } else {
+ serverURL = "http://" + meshHost + "/derp"
+ }
+ meshClient, err := derphttp.NewClient(derpServer.PrivateKey(), serverURL, 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 meshDialer.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 *Service) Close() error {
+ return common.Close(
+ common.PtrOrNil(d.listener),
+ 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: +
+ +