mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-08 03:34:13 +08:00
Add DERP service
This commit is contained in:
parent
99a09a6ce5
commit
1a8f6e053d
@ -25,6 +25,7 @@ const (
|
|||||||
TypeTUIC = "tuic"
|
TypeTUIC = "tuic"
|
||||||
TypeHysteria2 = "hysteria2"
|
TypeHysteria2 = "hysteria2"
|
||||||
TypeTailscale = "tailscale"
|
TypeTailscale = "tailscale"
|
||||||
|
TypeDERP = "derp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -6,7 +6,7 @@ icon: material/new-box
|
|||||||
|
|
||||||
# Endpoint
|
# Endpoint
|
||||||
|
|
||||||
Endpoint is protocols that has both inbound and outbound behavior.
|
An endpoint is a protocol with inbound and outbound behavior.
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ sing-box uses JSON for configuration files.
|
|||||||
"inbounds": [],
|
"inbounds": [],
|
||||||
"outbounds": [],
|
"outbounds": [],
|
||||||
"route": {},
|
"route": {},
|
||||||
|
"services": [],
|
||||||
"experimental": {}
|
"experimental": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -30,6 +31,7 @@ sing-box uses JSON for configuration files.
|
|||||||
| `inbounds` | [Inbound](./inbound/) |
|
| `inbounds` | [Inbound](./inbound/) |
|
||||||
| `outbounds` | [Outbound](./outbound/) |
|
| `outbounds` | [Outbound](./outbound/) |
|
||||||
| `route` | [Route](./route/) |
|
| `route` | [Route](./route/) |
|
||||||
|
| `services` | [Service](./service/) |
|
||||||
| `experimental` | [Experimental](./experimental/) |
|
| `experimental` | [Experimental](./experimental/) |
|
||||||
|
|
||||||
### Check
|
### Check
|
||||||
|
@ -14,6 +14,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
|||||||
"inbounds": [],
|
"inbounds": [],
|
||||||
"outbounds": [],
|
"outbounds": [],
|
||||||
"route": {},
|
"route": {},
|
||||||
|
"services": [],
|
||||||
"experimental": {}
|
"experimental": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -30,6 +31,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
|||||||
| `inbounds` | [入站](./inbound/) |
|
| `inbounds` | [入站](./inbound/) |
|
||||||
| `outbounds` | [出站](./outbound/) |
|
| `outbounds` | [出站](./outbound/) |
|
||||||
| `route` | [路由](./route/) |
|
| `route` | [路由](./route/) |
|
||||||
|
| `services` | [服务](./service/) |
|
||||||
| `experimental` | [实验性](./experimental/) |
|
| `experimental` | [实验性](./experimental/) |
|
||||||
|
|
||||||
### 检查
|
### 检查
|
||||||
|
130
docs/configuration/service/derp.md
Normal file
130
docs/configuration/service/derp.md
Normal file
@ -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__ }
|
||||||
|
```
|
30
docs/configuration/service/index.md
Normal file
30
docs/configuration/service/index.md
Normal file
@ -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.
|
2
go.mod
2
go.mod
@ -6,6 +6,7 @@ require (
|
|||||||
github.com/anytls/sing-anytls v0.0.8
|
github.com/anytls/sing-anytls v0.0.8
|
||||||
github.com/caddyserver/certmagic v0.23.0
|
github.com/caddyserver/certmagic v0.23.0
|
||||||
github.com/cloudflare/circl v1.6.1
|
github.com/cloudflare/circl v1.6.1
|
||||||
|
github.com/coder/websocket v1.8.12
|
||||||
github.com/cretz/bine v0.2.0
|
github.com/cretz/bine v0.2.0
|
||||||
github.com/go-chi/chi/v5 v5.2.1
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/go-chi/render v1.0.3
|
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/bits-and-blooms/bitset v1.13.0 // indirect
|
||||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // 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/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
"github.com/sagernet/sing-box/adapter/inbound"
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
"github.com/sagernet/sing-box/adapter/outbound"
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
"github.com/sagernet/sing-box/dns/transport"
|
"github.com/sagernet/sing-box/dns/transport"
|
||||||
@ -120,6 +121,9 @@ func DNSTransportRegistry() *dns.TransportRegistry {
|
|||||||
|
|
||||||
func ServiceRegistry() *service.Registry {
|
func ServiceRegistry() *service.Registry {
|
||||||
registry := service.NewRegistry()
|
registry := service.NewRegistry()
|
||||||
|
|
||||||
|
registerDERPService(registry)
|
||||||
|
|
||||||
return registry
|
return registry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,8 +4,10 @@ package include
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
"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/dns"
|
||||||
"github.com/sagernet/sing-box/protocol/tailscale"
|
"github.com/sagernet/sing-box/protocol/tailscale"
|
||||||
|
"github.com/sagernet/sing-box/service/derp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
||||||
@ -15,3 +17,7 @@ func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
|||||||
func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
||||||
tailscale.RegistryTransport(registry)
|
tailscale.RegistryTransport(registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerDERPService(registry *service.Registry) {
|
||||||
|
derp.Register(registry)
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
"github.com/sagernet/sing-box/log"
|
"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`)
|
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`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -169,6 +169,9 @@ nav:
|
|||||||
- DNS: configuration/outbound/dns.md
|
- DNS: configuration/outbound/dns.md
|
||||||
- Selector: configuration/outbound/selector.md
|
- Selector: configuration/outbound/selector.md
|
||||||
- URLTest: configuration/outbound/urltest.md
|
- URLTest: configuration/outbound/urltest.md
|
||||||
|
- Service:
|
||||||
|
- configuration/service/index.md
|
||||||
|
- DERP: configuration/service/derp.md
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- pymdownx.inlinehilite
|
- pymdownx.inlinehilite
|
||||||
- pymdownx.snippets
|
- pymdownx.snippets
|
||||||
|
@ -2,6 +2,12 @@ package option
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"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 {
|
type TailscaleEndpointOptions struct {
|
||||||
@ -23,3 +29,87 @@ type TailscaleDNSServerOptions struct {
|
|||||||
Endpoint string `json:"endpoint,omitempty"`
|
Endpoint string `json:"endpoint,omitempty"`
|
||||||
AcceptDefaultResolvers bool `json:"accept_default_resolvers,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))
|
||||||
|
}
|
||||||
|
518
service/derp/service.go
Normal file
518
service/derp/service.go
Normal file
@ -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 = `
|
||||||
|
<h1>DERP</h1>
|
||||||
|
<p>
|
||||||
|
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic
|
||||||
|
for Tailscale clients.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Documentation:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
|
||||||
|
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
|
||||||
|
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
|
func getHomeHandler(val string) (_ http.Handler, ok bool) {
|
||||||
|
if val == "" {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
w.Write([]byte(homePage))
|
||||||
|
}), true
|
||||||
|
}
|
||||||
|
if val == "blank" {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}), true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(val, "http://") || strings.HasPrefix(val, "https://") {
|
||||||
|
return http.RedirectHandler(val, http.StatusFound), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||||
|
|
||||||
|
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
|
||||||
|
// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
|
||||||
|
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
|
||||||
|
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
|
||||||
|
base.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||||
|
Subprotocols: []string{"derp"},
|
||||||
|
OriginPatterns: []string{"*"},
|
||||||
|
// Disable compression because we transmit WireGuard messages that
|
||||||
|
// are not compressible.
|
||||||
|
// Additionally, Safari has a broken implementation of compression
|
||||||
|
// (see https://github.com/nhooyr/websocket/issues/218) that makes
|
||||||
|
// enabling it actively harmful.
|
||||||
|
CompressionMode: websocket.CompressionDisabled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close(websocket.StatusInternalError, "closing")
|
||||||
|
if c.Subprotocol() != "derp" {
|
||||||
|
c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary, r.RemoteAddr)
|
||||||
|
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
|
||||||
|
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleBootstrapDNS(ctx context.Context) http.HandlerFunc {
|
||||||
|
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Connection", "close")
|
||||||
|
if queryDomain := r.URL.Query().Get("q"); queryDomain != "" {
|
||||||
|
addresses, err := dnsRouter.Lookup(ctx, queryDomain, adapter.DNSQueryOptions{})
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
render.JSON(w, r, render.M{
|
||||||
|
queryDomain: addresses,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write([]byte("{}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type derpConfig struct {
|
||||||
|
PrivateKey key.NodePrivate
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDERPConfig(path string) (*derpConfig, error) {
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return writeNewDERPConfig(path)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var config derpConfig
|
||||||
|
err = json.Unmarshal(content, &config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeNewDERPConfig(path string) (*derpConfig, error) {
|
||||||
|
newKey := key.NewNode()
|
||||||
|
err := os.MkdirAll(filepath.Dir(path), 0o777)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config := derpConfig{
|
||||||
|
PrivateKey: newKey,
|
||||||
|
}
|
||||||
|
content, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path, content, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Service) loopSTUNPacket(packetConn *net.UDPConn) {
|
||||||
|
buffer := make([]byte, 65535)
|
||||||
|
oob := make([]byte, 1024)
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
oobN int
|
||||||
|
addrPort netip.AddrPort
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
n, oobN, _, addrPort, err = packetConn.ReadMsgUDPAddrPort(buffer, oob)
|
||||||
|
if err != nil {
|
||||||
|
if E.IsClosedOrCanceled(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !stun.Is(buffer[:n]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
txid, err := stun.ParseBindingRequest(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packetConn.WriteMsgUDPAddrPort(stun.Response(txid, addrPort), oob[:oobN], addrPort)
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,7 @@ func TestMain(m *testing.M) {
|
|||||||
var globalCtx context.Context
|
var globalCtx context.Context
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
globalCtx = box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
|
globalCtx = box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
|
||||||
}
|
}
|
||||||
|
|
||||||
func startInstance(t *testing.T, options option.Options) *box.Box {
|
func startInstance(t *testing.T, options option.Options) *box.Box {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user