mirror of
https://github.com/juanfont/headscale.git
synced 2024-12-27 06:35:52 -05:00
Merge branch 'main' into main
This commit is contained in:
commit
e54c508c10
@ -3,6 +3,7 @@
|
|||||||
// development
|
// development
|
||||||
integration_test.go
|
integration_test.go
|
||||||
integration_test/
|
integration_test/
|
||||||
|
!integration_test/etc_embedded_derp/tls/server.crt
|
||||||
|
|
||||||
Dockerfile*
|
Dockerfile*
|
||||||
docker-compose*
|
docker-compose*
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
|
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
|
||||||
- Add shorthand aliases for commands and subcommands [#376](https://github.com/juanfont/headscale/pull/376)
|
- Add shorthand aliases for commands and subcommands [#376](https://github.com/juanfont/headscale/pull/376)
|
||||||
- Add `/windows` endpoint for Windows configuration instructions + registry file download [#392](https://github.com/juanfont/headscale/pull/392)
|
- Add `/windows` endpoint for Windows configuration instructions + registry file download [#392](https://github.com/juanfont/headscale/pull/392)
|
||||||
|
- Added embedded DERP server into Headscale [#388](https://github.com/juanfont/headscale/pull/388)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
@ -7,5 +7,10 @@ RUN apt-get update \
|
|||||||
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \
|
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \
|
||||||
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
&& curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y tailscale=${TAILSCALE_VERSION} dnsutils \
|
&& apt-get install -y ca-certificates tailscale=${TAILSCALE_VERSION} dnsutils \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ADD integration_test/etc_embedded_derp/tls/server.crt /usr/local/share/ca-certificates/
|
||||||
|
RUN chmod 644 /usr/local/share/ca-certificates/server.crt
|
||||||
|
|
||||||
|
RUN update-ca-certificates
|
||||||
|
3
Makefile
3
Makefile
@ -23,6 +23,9 @@ test_integration:
|
|||||||
test_integration_cli:
|
test_integration_cli:
|
||||||
go test -tags integration -v integration_cli_test.go integration_common_test.go
|
go test -tags integration -v integration_cli_test.go integration_common_test.go
|
||||||
|
|
||||||
|
test_integration_derp:
|
||||||
|
go test -tags integration -v integration_embedded_derp_test.go integration_common_test.go
|
||||||
|
|
||||||
coverprofile_func:
|
coverprofile_func:
|
||||||
go tool cover -func=coverage.out
|
go tool cover -func=coverage.out
|
||||||
|
|
||||||
|
@ -63,6 +63,7 @@ one of the maintainers.
|
|||||||
- Dual stack (IPv4 and IPv6)
|
- Dual stack (IPv4 and IPv6)
|
||||||
- Routing advertising (including exit nodes)
|
- Routing advertising (including exit nodes)
|
||||||
- Ephemeral nodes
|
- Ephemeral nodes
|
||||||
|
- Embedded [DERP server](https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp)
|
||||||
|
|
||||||
## Client OS support
|
## Client OS support
|
||||||
|
|
||||||
|
29
app.go
29
app.go
@ -120,6 +120,12 @@ type OIDCConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DERPConfig struct {
|
type DERPConfig struct {
|
||||||
|
ServerEnabled bool
|
||||||
|
ServerRegionID int
|
||||||
|
ServerRegionCode string
|
||||||
|
ServerRegionName string
|
||||||
|
STUNEnabled bool
|
||||||
|
STUNAddr string
|
||||||
URLs []url.URL
|
URLs []url.URL
|
||||||
Paths []string
|
Paths []string
|
||||||
AutoUpdate bool
|
AutoUpdate bool
|
||||||
@ -143,6 +149,7 @@ type Headscale struct {
|
|||||||
privateKey *key.MachinePrivate
|
privateKey *key.MachinePrivate
|
||||||
|
|
||||||
DERPMap *tailcfg.DERPMap
|
DERPMap *tailcfg.DERPMap
|
||||||
|
DERPServer *DERPServer
|
||||||
|
|
||||||
aclPolicy *ACLPolicy
|
aclPolicy *ACLPolicy
|
||||||
aclRules []tailcfg.FilterRule
|
aclRules []tailcfg.FilterRule
|
||||||
@ -178,7 +185,6 @@ func LookupTLSClientAuthMode(mode string) (tls.ClientAuthType, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHeadscale returns the Headscale app.
|
|
||||||
func NewHeadscale(cfg Config) (*Headscale, error) {
|
func NewHeadscale(cfg Config) (*Headscale, error) {
|
||||||
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -239,6 +245,14 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.DERP.ServerEnabled {
|
||||||
|
embeddedDERPServer, err := app.NewDERPServer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
app.DERPServer = embeddedDERPServer
|
||||||
|
}
|
||||||
|
|
||||||
return &app, nil
|
return &app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -463,6 +477,12 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine {
|
|||||||
router.GET("/swagger", SwaggerUI)
|
router.GET("/swagger", SwaggerUI)
|
||||||
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
|
router.GET("/swagger/v1/openapiv2.json", SwaggerAPIv1)
|
||||||
|
|
||||||
|
if h.cfg.DERP.ServerEnabled {
|
||||||
|
router.Any("/derp", h.DERPHandler)
|
||||||
|
router.Any("/derp/probe", h.DERPProbeHandler)
|
||||||
|
router.Any("/bootstrap-dns", h.DERPBootstrapDNSHandler)
|
||||||
|
}
|
||||||
|
|
||||||
api := router.Group("/api")
|
api := router.Group("/api")
|
||||||
api.Use(h.httpAuthenticationMiddleware)
|
api.Use(h.httpAuthenticationMiddleware)
|
||||||
{
|
{
|
||||||
@ -481,6 +501,13 @@ func (h *Headscale) Serve() error {
|
|||||||
// Fetch an initial DERP Map before we start serving
|
// Fetch an initial DERP Map before we start serving
|
||||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||||
|
|
||||||
|
if h.cfg.DERP.ServerEnabled {
|
||||||
|
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
||||||
|
if h.cfg.DERP.STUNEnabled {
|
||||||
|
go h.ServeSTUN()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if h.cfg.DERP.AutoUpdate {
|
if h.cfg.DERP.AutoUpdate {
|
||||||
derpMapCancelChannel := make(chan struct{})
|
derpMapCancelChannel := make(chan struct{})
|
||||||
defer func() { derpMapCancelChannel <- struct{}{} }()
|
defer func() { derpMapCancelChannel <- struct{}{} }()
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,12 +18,12 @@ var serveCmd = &cobra.Command{
|
|||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
h, err := getHeadscaleApp()
|
h, err := getHeadscaleApp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error initializing: %s", err)
|
log.Fatal().Caller().Err(err).Msg("Error initializing")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.Serve()
|
err = h.Serve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error initializing: %s", err)
|
log.Fatal().Caller().Err(err).Msg("Error starting server")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -117,6 +117,13 @@ func LoadConfig(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetDERPConfig() headscale.DERPConfig {
|
func GetDERPConfig() headscale.DERPConfig {
|
||||||
|
serverEnabled := viper.GetBool("derp.server.enabled")
|
||||||
|
serverRegionID := viper.GetInt("derp.server.region_id")
|
||||||
|
serverRegionCode := viper.GetString("derp.server.region_code")
|
||||||
|
serverRegionName := viper.GetString("derp.server.region_name")
|
||||||
|
stunEnabled := viper.GetBool("derp.server.stun.enabled")
|
||||||
|
stunAddr := viper.GetString("derp.server.stun.listen_addr")
|
||||||
|
|
||||||
urlStrs := viper.GetStringSlice("derp.urls")
|
urlStrs := viper.GetStringSlice("derp.urls")
|
||||||
|
|
||||||
urls := make([]url.URL, len(urlStrs))
|
urls := make([]url.URL, len(urlStrs))
|
||||||
@ -138,6 +145,12 @@ func GetDERPConfig() headscale.DERPConfig {
|
|||||||
updateFrequency := viper.GetDuration("derp.update_frequency")
|
updateFrequency := viper.GetDuration("derp.update_frequency")
|
||||||
|
|
||||||
return headscale.DERPConfig{
|
return headscale.DERPConfig{
|
||||||
|
ServerEnabled: serverEnabled,
|
||||||
|
ServerRegionID: serverRegionID,
|
||||||
|
ServerRegionCode: serverRegionCode,
|
||||||
|
ServerRegionName: serverRegionName,
|
||||||
|
STUNEnabled: stunEnabled,
|
||||||
|
STUNAddr: stunAddr,
|
||||||
URLs: urls,
|
URLs: urls,
|
||||||
Paths: paths,
|
Paths: paths,
|
||||||
AutoUpdate: autoUpdate,
|
AutoUpdate: autoUpdate,
|
||||||
|
@ -55,6 +55,26 @@ ip_prefixes:
|
|||||||
# headscale needs a list of DERP servers that can be presented
|
# headscale needs a list of DERP servers that can be presented
|
||||||
# to the clients.
|
# to the clients.
|
||||||
derp:
|
derp:
|
||||||
|
server:
|
||||||
|
# If enabled, runs the embedded DERP server and merges it into the rest of the DERP config
|
||||||
|
# The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# Region ID to use for the embedded DERP server.
|
||||||
|
# The local DERP prevails if the region ID collides with other region ID coming from
|
||||||
|
# the regular DERP config.
|
||||||
|
region_id: 999
|
||||||
|
|
||||||
|
# Region code and name are displayed in the Tailscale UI to identify a DERP region
|
||||||
|
region_code: "headscale"
|
||||||
|
region_name: "Headscale Embedded DERP"
|
||||||
|
|
||||||
|
# If enabled, also listens in UDP at the configured address for STUN connections to help on NAT traversal
|
||||||
|
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
|
||||||
|
stun:
|
||||||
|
enabled: false
|
||||||
|
listen_addr: "0.0.0.0:3478"
|
||||||
|
|
||||||
# List of externally available DERP maps encoded in JSON
|
# List of externally available DERP maps encoded in JSON
|
||||||
urls:
|
urls:
|
||||||
- https://controlplane.tailscale.com/derpmap/default
|
- https://controlplane.tailscale.com/derpmap/default
|
||||||
|
1
derp.go
1
derp.go
@ -148,6 +148,7 @@ func (h *Headscale) scheduledDERPMapUpdateWorker(cancelChan <-chan struct{}) {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
log.Info().Msg("Fetching DERPMap updates")
|
log.Info().Msg("Fetching DERPMap updates")
|
||||||
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
h.DERPMap = GetDERPMap(h.cfg.DERP)
|
||||||
|
h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region
|
||||||
|
|
||||||
namespaces, err := h.ListNamespaces()
|
namespaces, err := h.ListNamespaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
233
derp_server.go
Normal file
233
derp_server.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"tailscale.com/derp"
|
||||||
|
"tailscale.com/net/stun"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fastStartHeader is the header (with value "1") that signals to the HTTP
|
||||||
|
// server that the DERP HTTP client does not want the HTTP 101 response
|
||||||
|
// headers and it will begin writing & reading the DERP protocol immediately
|
||||||
|
// following its HTTP request.
|
||||||
|
const fastStartHeader = "Derp-Fast-Start"
|
||||||
|
|
||||||
|
type DERPServer struct {
|
||||||
|
tailscaleDERP *derp.Server
|
||||||
|
region tailcfg.DERPRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) NewDERPServer() (*DERPServer, error) {
|
||||||
|
server := derp.NewServer(key.NodePrivate(*h.privateKey), log.Info().Msgf)
|
||||||
|
region, err := h.generateRegionLocalDERP()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DERPServer{server, region}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) generateRegionLocalDERP() (tailcfg.DERPRegion, error) {
|
||||||
|
serverURL, err := url.Parse(h.cfg.ServerURL)
|
||||||
|
if err != nil {
|
||||||
|
return tailcfg.DERPRegion{}, err
|
||||||
|
}
|
||||||
|
var host string
|
||||||
|
var port int
|
||||||
|
host, portStr, err := net.SplitHostPort(serverURL.Host)
|
||||||
|
if err != nil {
|
||||||
|
if serverURL.Scheme == "https" {
|
||||||
|
host = serverURL.Host
|
||||||
|
port = 443
|
||||||
|
} else {
|
||||||
|
host = serverURL.Host
|
||||||
|
port = 80
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
port, err = strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return tailcfg.DERPRegion{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localDERPregion := tailcfg.DERPRegion{
|
||||||
|
RegionID: h.cfg.DERP.ServerRegionID,
|
||||||
|
RegionCode: h.cfg.DERP.ServerRegionCode,
|
||||||
|
RegionName: h.cfg.DERP.ServerRegionName,
|
||||||
|
Avoid: false,
|
||||||
|
Nodes: []*tailcfg.DERPNode{
|
||||||
|
{
|
||||||
|
Name: fmt.Sprintf("%d", h.cfg.DERP.ServerRegionID),
|
||||||
|
RegionID: h.cfg.DERP.ServerRegionID,
|
||||||
|
HostName: host,
|
||||||
|
DERPPort: port,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.cfg.DERP.STUNEnabled {
|
||||||
|
_, portStr, err := net.SplitHostPort(h.cfg.DERP.STUNAddr)
|
||||||
|
if err != nil {
|
||||||
|
return tailcfg.DERPRegion{}, err
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return tailcfg.DERPRegion{}, err
|
||||||
|
}
|
||||||
|
localDERPregion.Nodes[0].STUNPort = port
|
||||||
|
}
|
||||||
|
|
||||||
|
return localDERPregion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) DERPHandler(ctx *gin.Context) {
|
||||||
|
log.Trace().Caller().Msgf("/derp request from %v", ctx.ClientIP())
|
||||||
|
up := strings.ToLower(ctx.Request.Header.Get("Upgrade"))
|
||||||
|
if up != "websocket" && up != "derp" {
|
||||||
|
if up != "" {
|
||||||
|
log.Warn().Caller().Msgf("Weird websockets connection upgrade: %q", up)
|
||||||
|
}
|
||||||
|
ctx.String(http.StatusUpgradeRequired, "DERP requires connection upgrade")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fastStart := ctx.Request.Header.Get(fastStartHeader) == "1"
|
||||||
|
|
||||||
|
hijacker, ok := ctx.Writer.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
log.Error().Caller().Msg("DERP requires Hijacker interface from Gin")
|
||||||
|
ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
netConn, conn, err := hijacker.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Caller().Err(err).Msgf("Hijack failed")
|
||||||
|
ctx.String(http.StatusInternalServerError, "HTTP does not support general TCP support")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fastStart {
|
||||||
|
pubKey := h.privateKey.Public()
|
||||||
|
pubKeyStr := pubKey.UntypedHexString() // nolint
|
||||||
|
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||||
|
"Upgrade: DERP\r\n"+
|
||||||
|
"Connection: Upgrade\r\n"+
|
||||||
|
"Derp-Version: %v\r\n"+
|
||||||
|
"Derp-Public-Key: %s\r\n\r\n",
|
||||||
|
derp.ProtocolVersion,
|
||||||
|
pubKeyStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.DERPServer.tailscaleDERP.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DERPProbeHandler is the endpoint that js/wasm clients hit to measure
|
||||||
|
// DERP latency, since they can't do UDP STUN queries.
|
||||||
|
func (h *Headscale) DERPProbeHandler(ctx *gin.Context) {
|
||||||
|
switch ctx.Request.Method {
|
||||||
|
case "HEAD", "GET":
|
||||||
|
ctx.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
default:
|
||||||
|
ctx.String(http.StatusMethodNotAllowed, "bogus probe method")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DERPBootstrapDNSHandler implements the /bootsrap-dns endpoint
|
||||||
|
// Described in https://github.com/tailscale/tailscale/issues/1405,
|
||||||
|
// this endpoint provides a way to help a client when it fails to start up
|
||||||
|
// because its DNS are broken.
|
||||||
|
// The initial implementation is here https://github.com/tailscale/tailscale/pull/1406
|
||||||
|
// They have a cache, but not clear if that is really necessary at Headscale, uh, scale.
|
||||||
|
// An example implementation is found here https://derp.tailscale.com/bootstrap-dns
|
||||||
|
func (h *Headscale) DERPBootstrapDNSHandler(ctx *gin.Context) {
|
||||||
|
dnsEntries := make(map[string][]net.IP)
|
||||||
|
|
||||||
|
resolvCtx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
var r net.Resolver
|
||||||
|
for _, region := range h.DERPMap.Regions {
|
||||||
|
for _, node := range region.Nodes { // we don't care if we override some nodes
|
||||||
|
addrs, err := r.LookupIP(resolvCtx, "ip", node.HostName)
|
||||||
|
if err != nil {
|
||||||
|
log.Trace().Caller().Err(err).Msgf("bootstrap DNS lookup failed %q", node.HostName)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dnsEntries[node.HostName] = addrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, dnsEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeSTUN starts a STUN server on the configured addr.
|
||||||
|
func (h *Headscale) ServeSTUN() {
|
||||||
|
packetConn, err := net.ListenPacket("udp", h.cfg.DERP.STUNAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Msgf("failed to open STUN listener: %v", err)
|
||||||
|
}
|
||||||
|
log.Info().Msgf("STUN server started at %s", packetConn.LocalAddr())
|
||||||
|
|
||||||
|
udpConn, ok := packetConn.(*net.UDPConn)
|
||||||
|
if !ok {
|
||||||
|
log.Fatal().Msg("STUN listener is not a UDP listener")
|
||||||
|
}
|
||||||
|
serverSTUNListener(context.Background(), udpConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverSTUNListener(ctx context.Context, packetConn *net.UDPConn) {
|
||||||
|
var buf [64 << 10]byte
|
||||||
|
var (
|
||||||
|
bytesRead int
|
||||||
|
udpAddr *net.UDPAddr
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
bytesRead, udpAddr, err = packetConn.ReadFromUDP(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Error().Caller().Err(err).Msgf("STUN ReadFrom")
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Trace().Caller().Msgf("STUN request from %v", udpAddr)
|
||||||
|
pkt := buf[:bytesRead]
|
||||||
|
if !stun.Is(pkt) {
|
||||||
|
log.Trace().Caller().Msgf("UDP packet is not STUN")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
txid, err := stun.ParseBindingRequest(pkt)
|
||||||
|
if err != nil {
|
||||||
|
log.Trace().Caller().Err(err).Msgf("STUN parse error")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res := stun.Response(txid, udpAddr.IP, uint16(udpAddr.Port))
|
||||||
|
_, err = packetConn.WriteTo(res, udpAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Trace().Caller().Err(err).Msgf("Issue writing to UDP")
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
go.mod
4
go.mod
@ -46,8 +46,10 @@ require (
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
github.com/atomicgo/cursor v0.0.1 // indirect
|
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/containerd/continuity v0.2.2 // indirect
|
github.com/containerd/continuity v0.2.2 // indirect
|
||||||
@ -100,6 +102,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.11 // indirect
|
github.com/mattn/go-sqlite3 v1.14.11 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
@ -134,6 +137,7 @@ require (
|
|||||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
|
9
go.sum
9
go.sum
@ -82,6 +82,8 @@ github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEV
|
|||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
|
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||||
|
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
@ -106,6 +108,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
|
|||||||
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
|
||||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||||
github.com/bufbuild/buf v0.37.0/go.mod h1:lQ1m2HkIaGOFba6w/aC3KYBHhKEOESP3gaAEpS3dAFM=
|
github.com/bufbuild/buf v0.37.0/go.mod h1:lQ1m2HkIaGOFba6w/aC3KYBHhKEOESP3gaAEpS3dAFM=
|
||||||
|
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029 h1:POmUHfxXdeyM8Aomg4tKDcwATCFuW+cYLkj6pwsw9pc=
|
||||||
|
github.com/ccding/go-stun/stun v0.0.0-20200514191101-4dc67bcdb029/go.mod h1:Rpr5n9cGHYdM3S3IK8ROSUUUYjQOu+MSUCZDcJbYWi8=
|
||||||
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
|
github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo=
|
||||||
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
@ -205,6 +209,7 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF
|
|||||||
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
|
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
|
||||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||||
|
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||||
@ -570,6 +575,8 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
|
|||||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
@ -1083,6 +1090,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
|
||||||
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
@ -6,6 +6,7 @@ package headscale
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ory/dockertest/v3"
|
"github.com/ory/dockertest/v3"
|
||||||
@ -18,8 +19,15 @@ const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second
|
|||||||
var (
|
var (
|
||||||
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
|
IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10")
|
||||||
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
|
IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48")
|
||||||
|
|
||||||
|
tailscaleVersions = []string{"1.22.0", "1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TestNamespace struct {
|
||||||
|
count int
|
||||||
|
tailscales map[string]dockertest.Resource
|
||||||
|
}
|
||||||
|
|
||||||
type ExecuteCommandConfig struct {
|
type ExecuteCommandConfig struct {
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
}
|
}
|
||||||
@ -119,3 +127,35 @@ func DockerAllowNetworkAdministration(config *docker.HostConfig) {
|
|||||||
Target: "/dev/net/tun",
|
Target: "/dev/net/tun",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getIPs(
|
||||||
|
tailscales map[string]dockertest.Resource,
|
||||||
|
) (map[string][]netaddr.IP, error) {
|
||||||
|
ips := make(map[string][]netaddr.IP)
|
||||||
|
for hostname, tailscale := range tailscales {
|
||||||
|
command := []string{"tailscale", "ip"}
|
||||||
|
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, address := range strings.Split(result, "\n") {
|
||||||
|
address = strings.TrimSuffix(address, "\n")
|
||||||
|
if len(address) < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ip, err := netaddr.ParseIP(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ips[hostname] = append(ips[hostname], ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ips, nil
|
||||||
|
}
|
||||||
|
396
integration_embedded_derp_test.go
Normal file
396
integration_embedded_derp_test.go
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package headscale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
|
"github.com/ory/dockertest/v3"
|
||||||
|
"github.com/ory/dockertest/v3/docker"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/ccding/go-stun/stun"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headscaleHostname = "headscale-derp"
|
||||||
|
namespaceName = "derpnamespace"
|
||||||
|
totalContainers = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type IntegrationDERPTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
stats *suite.SuiteInformation
|
||||||
|
|
||||||
|
pool dockertest.Pool
|
||||||
|
networks map[int]dockertest.Network // so we keep the containers isolated
|
||||||
|
headscale dockertest.Resource
|
||||||
|
|
||||||
|
tailscales map[string]dockertest.Resource
|
||||||
|
joinWaitGroup sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDERPIntegrationTestSuite(t *testing.T) {
|
||||||
|
s := new(IntegrationDERPTestSuite)
|
||||||
|
|
||||||
|
s.tailscales = make(map[string]dockertest.Resource)
|
||||||
|
s.networks = make(map[int]dockertest.Network)
|
||||||
|
|
||||||
|
suite.Run(t, s)
|
||||||
|
|
||||||
|
// HandleStats, which allows us to check if we passed and save logs
|
||||||
|
// is called after TearDown, so we cannot tear down containers before
|
||||||
|
// we have potentially saved the logs.
|
||||||
|
for _, tailscale := range s.tailscales {
|
||||||
|
if err := s.pool.Purge(&tailscale); err != nil {
|
||||||
|
log.Printf("Could not purge resource: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.stats.Passed() {
|
||||||
|
err := s.saveLog(&s.headscale, "test_output")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not save log: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.pool.Purge(&s.headscale); err != nil {
|
||||||
|
log.Printf("Could not purge resource: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, network := range s.networks {
|
||||||
|
if err := network.Close(); err != nil {
|
||||||
|
log.Printf("Could not close network: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationDERPTestSuite) SetupSuite() {
|
||||||
|
if ppool, err := dockertest.NewPool(""); err == nil {
|
||||||
|
s.pool = *ppool
|
||||||
|
} else {
|
||||||
|
log.Fatalf("Could not connect to docker: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < totalContainers; i++ {
|
||||||
|
if pnetwork, err := s.pool.CreateNetwork(fmt.Sprintf("headscale-derp-%d", i)); err == nil {
|
||||||
|
s.networks[i] = *pnetwork
|
||||||
|
} else {
|
||||||
|
log.Fatalf("Could not create network: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile",
|
||||||
|
ContextDir: ".",
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPath, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not determine current path: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headscaleOptions := &dockertest.RunOptions{
|
||||||
|
Name: headscaleHostname,
|
||||||
|
Mounts: []string{
|
||||||
|
fmt.Sprintf("%s/integration_test/etc_embedded_derp:/etc/headscale", currentPath),
|
||||||
|
},
|
||||||
|
Cmd: []string{"headscale", "serve"},
|
||||||
|
ExposedPorts: []string{"8443/tcp", "3478/udp"},
|
||||||
|
PortBindings: map[docker.Port][]docker.PortBinding{
|
||||||
|
"8443/tcp": {{HostPort: "8443"}},
|
||||||
|
"3478/udp": {{HostPort: "3478"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Creating headscale container")
|
||||||
|
if pheadscale, err := s.pool.BuildAndRunWithBuildOptions(headscaleBuildOptions, headscaleOptions, DockerRestartPolicy); err == nil {
|
||||||
|
s.headscale = *pheadscale
|
||||||
|
} else {
|
||||||
|
log.Fatalf("Could not start resource: %s", err)
|
||||||
|
}
|
||||||
|
log.Println("Created headscale container to test DERP")
|
||||||
|
|
||||||
|
log.Println("Creating tailscale containers")
|
||||||
|
|
||||||
|
for i := 0; i < totalContainers; i++ {
|
||||||
|
version := tailscaleVersions[i%len(tailscaleVersions)]
|
||||||
|
hostname, container := s.tailscaleContainer(
|
||||||
|
fmt.Sprint(i),
|
||||||
|
version,
|
||||||
|
s.networks[i],
|
||||||
|
)
|
||||||
|
s.tailscales[hostname] = *container
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Waiting for headscale to be ready")
|
||||||
|
hostEndpoint := fmt.Sprintf("localhost:%s", s.headscale.GetPort("8443/tcp"))
|
||||||
|
|
||||||
|
if err := s.pool.Retry(func() error {
|
||||||
|
url := fmt.Sprintf("https://%s/health", hostEndpoint)
|
||||||
|
insecureTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
insecureTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
|
client := &http.Client{Transport: insecureTransport}
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("status code not OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
// TODO(kradalby): If we cannot access headscale, or any other fatal error during
|
||||||
|
// test setup, we need to abort and tear down. However, testify does not seem to
|
||||||
|
// support that at the moment:
|
||||||
|
// https://github.com/stretchr/testify/issues/849
|
||||||
|
return // fmt.Errorf("Could not connect to headscale: %s", err)
|
||||||
|
}
|
||||||
|
log.Println("headscale container is ready")
|
||||||
|
|
||||||
|
log.Printf("Creating headscale namespace: %s\n", namespaceName)
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{"headscale", "namespaces", "create", namespaceName},
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
log.Println("headscale create namespace result: ", result)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
log.Printf("Creating pre auth key for %s\n", namespaceName)
|
||||||
|
preAuthResult, err := ExecuteCommand(
|
||||||
|
&s.headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"--namespace",
|
||||||
|
namespaceName,
|
||||||
|
"preauthkeys",
|
||||||
|
"create",
|
||||||
|
"--reusable",
|
||||||
|
"--expiration",
|
||||||
|
"24h",
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
},
|
||||||
|
[]string{"LOG_LEVEL=error"},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
|
||||||
|
var preAuthKey v1.PreAuthKey
|
||||||
|
err = json.Unmarshal([]byte(preAuthResult), &preAuthKey)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
assert.True(s.T(), preAuthKey.Reusable)
|
||||||
|
|
||||||
|
headscaleEndpoint := fmt.Sprintf("https://headscale:%s", s.headscale.GetPort("8443/tcp"))
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"Joining tailscale containers to headscale at %s\n",
|
||||||
|
headscaleEndpoint,
|
||||||
|
)
|
||||||
|
for hostname, tailscale := range s.tailscales {
|
||||||
|
s.joinWaitGroup.Add(1)
|
||||||
|
go s.Join(headscaleEndpoint, preAuthKey.Key, hostname, tailscale)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.joinWaitGroup.Wait()
|
||||||
|
|
||||||
|
// The nodes need a bit of time to get their updated maps from headscale
|
||||||
|
// TODO: See if we can have a more deterministic wait here.
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationDERPTestSuite) Join(
|
||||||
|
endpoint, key, hostname string,
|
||||||
|
tailscale dockertest.Resource,
|
||||||
|
) {
|
||||||
|
defer s.joinWaitGroup.Done()
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"tailscale",
|
||||||
|
"up",
|
||||||
|
"-login-server",
|
||||||
|
endpoint,
|
||||||
|
"--authkey",
|
||||||
|
key,
|
||||||
|
"--hostname",
|
||||||
|
hostname,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Join command:", command)
|
||||||
|
log.Printf("Running join command for %s\n", hostname)
|
||||||
|
_, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
log.Printf("%s joined\n", hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationDERPTestSuite) tailscaleContainer(identifier, version string, network dockertest.Network,
|
||||||
|
) (string, *dockertest.Resource) {
|
||||||
|
tailscaleBuildOptions := &dockertest.BuildOptions{
|
||||||
|
Dockerfile: "Dockerfile.tailscale",
|
||||||
|
ContextDir: ".",
|
||||||
|
BuildArgs: []docker.BuildArg{
|
||||||
|
{
|
||||||
|
Name: "TAILSCALE_VERSION",
|
||||||
|
Value: version,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
hostname := fmt.Sprintf(
|
||||||
|
"tailscale-%s-%s",
|
||||||
|
strings.Replace(version, ".", "-", -1),
|
||||||
|
identifier,
|
||||||
|
)
|
||||||
|
tailscaleOptions := &dockertest.RunOptions{
|
||||||
|
Name: hostname,
|
||||||
|
Networks: []*dockertest.Network{&network},
|
||||||
|
Cmd: []string{
|
||||||
|
"tailscaled", "--tun=tsdev",
|
||||||
|
},
|
||||||
|
|
||||||
|
// expose the host IP address, so we can access it from inside the container
|
||||||
|
ExtraHosts: []string{"host.docker.internal:host-gateway", "headscale:host-gateway"},
|
||||||
|
}
|
||||||
|
|
||||||
|
pts, err := s.pool.BuildAndRunWithBuildOptions(
|
||||||
|
tailscaleBuildOptions,
|
||||||
|
tailscaleOptions,
|
||||||
|
DockerRestartPolicy,
|
||||||
|
DockerAllowLocalIPv6,
|
||||||
|
DockerAllowNetworkAdministration,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not start resource: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("Created %s container\n", hostname)
|
||||||
|
|
||||||
|
return hostname, pts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationDERPTestSuite) TearDownSuite() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationDERPTestSuite) HandleStats(
|
||||||
|
suiteName string,
|
||||||
|
stats *suite.SuiteInformation,
|
||||||
|
) {
|
||||||
|
s.stats = stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationDERPTestSuite) saveLog(
|
||||||
|
resource *dockertest.Resource,
|
||||||
|
basePath string,
|
||||||
|
) error {
|
||||||
|
err := os.MkdirAll(basePath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
|
err = s.pool.Client.Logs(
|
||||||
|
docker.LogsOptions{
|
||||||
|
Context: context.TODO(),
|
||||||
|
Container: resource.Container.ID,
|
||||||
|
OutputStream: &stdout,
|
||||||
|
ErrorStream: &stderr,
|
||||||
|
Tail: "all",
|
||||||
|
RawTerminal: false,
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
Follow: false,
|
||||||
|
Timestamps: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(
|
||||||
|
path.Join(basePath, resource.Container.Name+".stdout.log"),
|
||||||
|
[]byte(stdout.String()),
|
||||||
|
0o644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(
|
||||||
|
path.Join(basePath, resource.Container.Name+".stderr.log"),
|
||||||
|
[]byte(stdout.String()),
|
||||||
|
0o644,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationDERPTestSuite) TestPingAllPeersByHostname() {
|
||||||
|
ips, err := getIPs(s.tailscales)
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
for hostname, tailscale := range s.tailscales {
|
||||||
|
for peername := range ips {
|
||||||
|
if peername == hostname {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) {
|
||||||
|
command := []string{
|
||||||
|
"tailscale", "ping",
|
||||||
|
"--timeout=10s",
|
||||||
|
"--c=5",
|
||||||
|
"--until-direct=false",
|
||||||
|
peername,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"Pinging using hostname from %s to %s\n",
|
||||||
|
hostname,
|
||||||
|
peername,
|
||||||
|
)
|
||||||
|
log.Println(command)
|
||||||
|
result, err := ExecuteCommand(
|
||||||
|
&tailscale,
|
||||||
|
command,
|
||||||
|
[]string{},
|
||||||
|
)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
log.Printf("Result for %s: %s\n", hostname, result)
|
||||||
|
assert.Contains(t, result, "via DERP(headscale)")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *IntegrationDERPTestSuite) TestDERPSTUN() {
|
||||||
|
headscaleSTUNAddr := fmt.Sprintf("localhost:%s", s.headscale.GetPort("3478/udp"))
|
||||||
|
client := stun.NewClient()
|
||||||
|
client.SetVerbose(true)
|
||||||
|
client.SetVVerbose(true)
|
||||||
|
client.SetServerAddr(headscaleSTUNAddr)
|
||||||
|
_, _, err := client.Discover()
|
||||||
|
assert.Nil(s.T(), err)
|
||||||
|
}
|
@ -29,13 +29,6 @@ import (
|
|||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
)
|
)
|
||||||
|
|
||||||
var tailscaleVersions = []string{"1.20.4", "1.18.2", "1.16.2", "1.14.3", "1.12.3"}
|
|
||||||
|
|
||||||
type TestNamespace struct {
|
|
||||||
count int
|
|
||||||
tailscales map[string]dockertest.Resource
|
|
||||||
}
|
|
||||||
|
|
||||||
type IntegrationTestSuite struct {
|
type IntegrationTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
stats *suite.SuiteInformation
|
stats *suite.SuiteInformation
|
||||||
@ -687,38 +680,6 @@ func (s *IntegrationTestSuite) TestMagicDNS() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIPs(
|
|
||||||
tailscales map[string]dockertest.Resource,
|
|
||||||
) (map[string][]netaddr.IP, error) {
|
|
||||||
ips := make(map[string][]netaddr.IP)
|
|
||||||
for hostname, tailscale := range tailscales {
|
|
||||||
command := []string{"tailscale", "ip"}
|
|
||||||
|
|
||||||
result, err := ExecuteCommand(
|
|
||||||
&tailscale,
|
|
||||||
command,
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, address := range strings.Split(result, "\n") {
|
|
||||||
address = strings.TrimSuffix(address, "\n")
|
|
||||||
if len(address) < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ip, err := netaddr.ParseIP(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ips[hostname] = append(ips[hostname], ip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAPIURLs(
|
func getAPIURLs(
|
||||||
tailscales map[string]dockertest.Resource,
|
tailscales map[string]dockertest.Resource,
|
||||||
) (map[netaddr.IP]string, error) {
|
) (map[netaddr.IP]string, error) {
|
||||||
|
29
integration_test/etc_embedded_derp/config.yaml
Normal file
29
integration_test/etc_embedded_derp/config.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
log_level: trace
|
||||||
|
acl_policy_path: ""
|
||||||
|
db_type: sqlite3
|
||||||
|
ephemeral_node_inactivity_timeout: 30m
|
||||||
|
ip_prefixes:
|
||||||
|
- fd7a:115c:a1e0::/48
|
||||||
|
- 100.64.0.0/10
|
||||||
|
dns_config:
|
||||||
|
base_domain: headscale.net
|
||||||
|
magic_dns: true
|
||||||
|
domains: []
|
||||||
|
nameservers:
|
||||||
|
- 1.1.1.1
|
||||||
|
db_path: /tmp/integration_test_db.sqlite3
|
||||||
|
private_key_path: private.key
|
||||||
|
listen_addr: 0.0.0.0:8443
|
||||||
|
server_url: https://headscale:8443
|
||||||
|
tls_cert_path: "/etc/headscale/tls/server.crt"
|
||||||
|
tls_key_path: "/etc/headscale/tls/server.key"
|
||||||
|
tls_client_auth_mode: disabled
|
||||||
|
derp:
|
||||||
|
server:
|
||||||
|
enabled: true
|
||||||
|
region_id: 999
|
||||||
|
region_code: "headscale"
|
||||||
|
region_name: "Headscale Embedded DERP"
|
||||||
|
stun:
|
||||||
|
enabled: true
|
||||||
|
listen_addr: "0.0.0.0:3478"
|
22
integration_test/etc_embedded_derp/tls/server.crt
Normal file
22
integration_test/etc_embedded_derp/tls/server.crt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIC8jCCAdqgAwIBAgIULbu+UbSTMG/LtxooLLh7BgSEyqEwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFDESMBAGA1UEAwwJaGVhZHNjYWxlMCAXDTIyMDMwNTE2NDgwM1oYDzI1MjEx
|
||||||
|
MTA0MTY0ODAzWjAUMRIwEAYDVQQDDAloZWFkc2NhbGUwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQDqcfpToLZUF0rlNwXkkt3lbyw4Cl4TJdx36o2PKaOK
|
||||||
|
U+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1WATuQJlMeg+2UJXGaTGRKkkbPMy3
|
||||||
|
5m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6sXmNeETJvBixpBev9yKJuVXgqHNS4
|
||||||
|
NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH14rav8Uimonl8UTNVXufMzyUOuoaQ
|
||||||
|
TGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3uQJXy0m8I6OrIoXLNxnqYMfFls79
|
||||||
|
9SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJRG1E3ZiLAgMBAAGjOjA4MBQGA1Ud
|
||||||
|
EQQNMAuCCWhlYWRzY2FsZTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH
|
||||||
|
AwEwDQYJKoZIhvcNAQELBQADggEBANGlVN7NCsJaKz0k0nhlRGK+tcxn2p1PXN/i
|
||||||
|
Iy+JX8ahixPC4ocRwOhrXgb390ZXLLwq08HrWYRB/Wi1VUzCp5d8dVxvrR43dJ+v
|
||||||
|
L2EOBiIKgcu2C3pWW1qRR46/EoXUU9kSH2VNBvIhNufi32kEOidoDzxtQf6qVCoF
|
||||||
|
guUt1JkAqrynv1UvR/2ZRM/WzM/oJ8qfECwrwDxyYhkqU5Z5jCWg0C6kPIBvNdzt
|
||||||
|
B0eheWS+ZxVwkePTR4e17kIafwknth3lo+orxVrq/xC+OVM1bGrt2ZyD64ZvEqQl
|
||||||
|
w6kgbzBdLScAQptWOFThwhnJsg0UbYKimZsnYmjVEuN59TJv92M=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
|
||||||
|
(Expires on Nov 4 16:48:03 2521 GMT)
|
||||||
|
|
28
integration_test/etc_embedded_derp/tls/server.key
Normal file
28
integration_test/etc_embedded_derp/tls/server.key
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDqcfpToLZUF0rl
|
||||||
|
NwXkkt3lbyw4Cl4TJdx36o2PKaOKU+tze/IjRsCWeMwrcR1o9TNZcxsD+c2J48D1
|
||||||
|
WATuQJlMeg+2UJXGaTGRKkkbPMy35m7AFf/Q16UEOgm2NYjZaQ8faRGIMYURG/6s
|
||||||
|
XmNeETJvBixpBev9yKJuVXgqHNS4NpEkNwdOCuAZXrmw0HCbiusawJOay4tFvhH1
|
||||||
|
4rav8Uimonl8UTNVXufMzyUOuoaQTGflmzYX3hIoswRnTPlIWFoqObvx2Q8H+of3
|
||||||
|
uQJXy0m8I6OrIoXLNxnqYMfFls799SYgVc2jPsCbh5fwyRbx2Hof7sIZ1K/mNgxJ
|
||||||
|
RG1E3ZiLAgMBAAECggEBALu1Ni/u5Qy++YA8ZcN0s6UXNdhItLmv/q0kZuLQ+9et
|
||||||
|
CT8VZfFInLndTdsaXenDKLHdryunviFA8SV+q7P2lMbek+Xs735EiyMnMBFWxLIZ
|
||||||
|
FWNGOeQERGL19QCmLEOmEi2b+iWJQHlKaMWpbPXL3w11a+lKjIBNO4ALfoJ5QveZ
|
||||||
|
cGMKsJdm/mpqBvLeNeh2eAFk3Gp6sT1g80Ge8NkgyzFBNIqnut0eerM15kPTc6Qz
|
||||||
|
12JLaOXUuV3PrcB4PN4nOwrTDg88GDNOQtc1Pc9r4nOHyLfr8X7QEtj1wXSwmOuK
|
||||||
|
d6ynMnAmoxVA9wEnupLbil1bzohRzpsTpkmDruYaBEECgYEA/Z09I8D6mt2NVqIE
|
||||||
|
KyvLjBK39ijSV9r3/lvB2Ple2OOL5YQEd+yTrIFy+3zdUnDgD1zmNnXjmjvHZ9Lc
|
||||||
|
IFf2o06AF84QLNB5gLPdDQkGNFdDqUxljBrfAfE3oANmPS/B0SijMGOOOiDO2FtO
|
||||||
|
xl1nfRr78mswuRs9awoUWCdNRKUCgYEA7KaTYKIQW/FEjw9lshp74q5vbn6zoXF5
|
||||||
|
7N8VkwI+bBVNvRbM9XZ8qhfgRdu9eXs5oL/N4mSYY54I8fA//pJ0Z2vpmureMm1V
|
||||||
|
mL5WBUmSD9DIbAchoK+sRiQhVmNMBQC6cHMABA7RfXvBeGvWrm9pKCS6ZLgLjkjp
|
||||||
|
PsmAcaXQcW8CgYEA2inAxljjOwUK6FNGsrxhxIT1qtNC3kCGxE+6WSNq67gSR8Vg
|
||||||
|
8qiX//T7LEslOB3RIGYRwxd2St7RkgZZRZllmOWWWuPwFhzf6E7RAL2akLvggGov
|
||||||
|
kG4tGEagSw2hjVDfsUT73ExHtMk0Jfmlsg33UC8+PDLpHtLH6qQpDAwC8+ECgYEA
|
||||||
|
o+AqOIWhvHmT11l7O915Ip1WzvZwYADbxLsrDnVEUsZh4epTHjvh0kvcY6PqTqCV
|
||||||
|
ZIrOANNWb811Nkz/k8NJVoD08PFp0xPBbZeIq/qpachTsfMyRzq/mobUiyUR9Hjv
|
||||||
|
ooUQYr78NOApNsG+lWbTNBhS9wI4BlzZIECbcJe5g4MCgYEAndRoy8S+S0Hx/S8a
|
||||||
|
O3hzXeDmivmgWqn8NVD4AKOovpkz4PaIVVQbAQkiNfAx8/DavPvjEKAbDezJ4ECV
|
||||||
|
j7IsOWtDVI7pd6eF9fTcECwisrda8aUoiOap8AQb48153Vx+g2N4Vy3uH0xJs4cz
|
||||||
|
TDALZPOBg8VlV+HEFDP43sp9Bf0=
|
||||||
|
-----END PRIVATE KEY-----
|
Loading…
Reference in New Issue
Block a user