mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-03-31 18:37:36 +03:00
Add DERP inbound
This commit is contained in:
parent
68df19ae13
commit
17962f5e65
10 changed files with 555 additions and 19 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ const (
|
|||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeDERP = "derp"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
6
go.mod
6
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
|
||||
)
|
||||
|
|
19
go.sum
19
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=
|
||||
|
|
|
@ -58,6 +58,7 @@ func InboundRegistry() *inbound.Registry {
|
|||
|
||||
registerQUICInbounds(registry)
|
||||
registerStubForRemovedInbounds(registry)
|
||||
registerTailscaleInbound(registry)
|
||||
|
||||
return registry
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
487
protocol/tailscale/derp.go
Normal file
487
protocol/tailscale/derp.go
Normal file
|
@ -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 = `
|
||||
<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, queryOptions adapter.DNSQueryOptions) 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, queryOptions)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render.JSON(w, r, render.M{
|
||||
queryDomain: addresses,
|
||||
})
|
||||
return
|
||||
}
|
||||
w.Write([]byte("{}"))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DERPInbound) loopSTUN(packetConn *net.UDPConn) {
|
||||
var buffer [64 << 10]byte
|
||||
var (
|
||||
n int
|
||||
addrPort netip.AddrPort
|
||||
err error
|
||||
)
|
||||
for {
|
||||
n, addrPort, err = packetConn.ReadFromUDPAddrPort(buffer[:])
|
||||
if err != nil {
|
||||
if E.IsClosedOrCanceled(err) {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
pkt := buffer[:n]
|
||||
if !stun.Is(pkt) {
|
||||
continue
|
||||
}
|
||||
txid, err := stun.ParseBindingRequest(pkt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
packetConn.WriteToUDPAddrPort(stun.Response(txid, addrPort), addrPort)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -149,6 +149,17 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
|
||||
},
|
||||
DNS: &dnsConfigurtor{},
|
||||
HTTPClient: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return &Endpoint{
|
||||
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
|
||||
|
@ -214,18 +225,6 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
|
|||
t.stack = ipStack
|
||||
|
||||
localBackend := t.server.ExportLocalBackend()
|
||||
localBackend.SetHTTPTestClient(&http.Client{
|
||||
Transport: &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return t.server.Dialer.DialContext(ctx, network, M.ParseSocksaddr(address))
|
||||
},
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: adapter.RootPoolFromContext(t.ctx),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
perfs := &ipn.MaskedPrefs{
|
||||
ExitNodeIPSet: true,
|
||||
AdvertiseRoutesSet: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue