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

+ +