mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Merge branch 'main' into main
This commit is contained in:
		
						commit
						e54c508c10
					
				| @ -3,6 +3,7 @@ | ||||
| // development | ||||
| integration_test.go | ||||
| integration_test/ | ||||
| !integration_test/etc_embedded_derp/tls/server.crt | ||||
| 
 | ||||
| Dockerfile* | ||||
| docker-compose* | ||||
|  | ||||
| @ -19,6 +19,7 @@ | ||||
| - 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 `/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 | ||||
| 
 | ||||
|  | ||||
| @ -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.list | tee /etc/apt/sources.list.d/tailscale.list \ | ||||
|     && 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/* | ||||
| 
 | ||||
| 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: | ||||
| 	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: | ||||
| 	go tool cover -func=coverage.out | ||||
| 
 | ||||
|  | ||||
| @ -63,6 +63,7 @@ one of the maintainers. | ||||
| - Dual stack (IPv4 and IPv6) | ||||
| - Routing advertising (including exit nodes) | ||||
| - Ephemeral nodes | ||||
| - Embedded [DERP server](https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp) | ||||
| 
 | ||||
| ## Client OS support | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										39
									
								
								app.go
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								app.go
									
									
									
									
									
								
							| @ -120,10 +120,16 @@ type OIDCConfig struct { | ||||
| } | ||||
| 
 | ||||
| type DERPConfig struct { | ||||
| 	URLs            []url.URL | ||||
| 	Paths           []string | ||||
| 	AutoUpdate      bool | ||||
| 	UpdateFrequency time.Duration | ||||
| 	ServerEnabled    bool | ||||
| 	ServerRegionID   int | ||||
| 	ServerRegionCode string | ||||
| 	ServerRegionName string | ||||
| 	STUNEnabled      bool | ||||
| 	STUNAddr         string | ||||
| 	URLs             []url.URL | ||||
| 	Paths            []string | ||||
| 	AutoUpdate       bool | ||||
| 	UpdateFrequency  time.Duration | ||||
| } | ||||
| 
 | ||||
| type CLIConfig struct { | ||||
| @ -142,7 +148,8 @@ type Headscale struct { | ||||
| 	dbDebug    bool | ||||
| 	privateKey *key.MachinePrivate | ||||
| 
 | ||||
| 	DERPMap *tailcfg.DERPMap | ||||
| 	DERPMap    *tailcfg.DERPMap | ||||
| 	DERPServer *DERPServer | ||||
| 
 | ||||
| 	aclPolicy *ACLPolicy | ||||
| 	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) { | ||||
| 	privKey, err := readOrCreatePrivateKey(cfg.PrivateKeyPath) | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| @ -463,6 +477,12 @@ func (h *Headscale) createRouter(grpcMux *runtime.ServeMux) *gin.Engine { | ||||
| 	router.GET("/swagger", SwaggerUI) | ||||
| 	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.Use(h.httpAuthenticationMiddleware) | ||||
| 	{ | ||||
| @ -481,6 +501,13 @@ func (h *Headscale) Serve() error { | ||||
| 	// Fetch an initial DERP Map before we start serving
 | ||||
| 	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 { | ||||
| 		derpMapCancelChannel := make(chan struct{}) | ||||
| 		defer func() { derpMapCancelChannel <- struct{}{} }() | ||||
|  | ||||
| @ -1,8 +1,7 @@ | ||||
| package cli | ||||
| 
 | ||||
| import ( | ||||
| 	"log" | ||||
| 
 | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
| 
 | ||||
| @ -19,12 +18,12 @@ var serveCmd = &cobra.Command{ | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		h, err := getHeadscaleApp() | ||||
| 		if err != nil { | ||||
| 			log.Fatalf("Error initializing: %s", err) | ||||
| 			log.Fatal().Caller().Err(err).Msg("Error initializing") | ||||
| 		} | ||||
| 
 | ||||
| 		err = h.Serve() | ||||
| 		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 { | ||||
| 	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") | ||||
| 
 | ||||
| 	urls := make([]url.URL, len(urlStrs)) | ||||
| @ -138,10 +145,16 @@ func GetDERPConfig() headscale.DERPConfig { | ||||
| 	updateFrequency := viper.GetDuration("derp.update_frequency") | ||||
| 
 | ||||
| 	return headscale.DERPConfig{ | ||||
| 		URLs:            urls, | ||||
| 		Paths:           paths, | ||||
| 		AutoUpdate:      autoUpdate, | ||||
| 		UpdateFrequency: updateFrequency, | ||||
| 		ServerEnabled:    serverEnabled, | ||||
| 		ServerRegionID:   serverRegionID, | ||||
| 		ServerRegionCode: serverRegionCode, | ||||
| 		ServerRegionName: serverRegionName, | ||||
| 		STUNEnabled:      stunEnabled, | ||||
| 		STUNAddr:         stunAddr, | ||||
| 		URLs:             urls, | ||||
| 		Paths:            paths, | ||||
| 		AutoUpdate:       autoUpdate, | ||||
| 		UpdateFrequency:  updateFrequency, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -55,6 +55,26 @@ ip_prefixes: | ||||
| # headscale needs a list of DERP servers that can be presented | ||||
| # to the clients. | ||||
| 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 | ||||
|   urls: | ||||
|     - 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: | ||||
| 			log.Info().Msg("Fetching DERPMap updates") | ||||
| 			h.DERPMap = GetDERPMap(h.cfg.DERP) | ||||
| 			h.DERPMap.Regions[h.DERPServer.region.RegionID] = &h.DERPServer.region | ||||
| 
 | ||||
| 			namespaces, err := h.ListNamespaces() | ||||
| 			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/Microsoft/go-winio v0.5.2 // 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/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/cespare/xxhash/v2 v2.1.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/matttproud/golang_protobuf_extensions v1.0.1 // 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/moby/term v0.0.0-20210619224110-3f7ff695adc6 // 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/term v0.0.0-20210927222741-03fcf44c2211 // 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 | ||||
| 	gopkg.in/ini.v1 v1.66.4 // 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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= | ||||
| 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-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= | ||||
| 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/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/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/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= | ||||
| 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/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.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= | ||||
| 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/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/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-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/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= | ||||
| 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-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-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-20180917221912-90fa682c2a6e/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 ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/ory/dockertest/v3" | ||||
| @ -18,8 +19,15 @@ const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second | ||||
| var ( | ||||
| 	IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10") | ||||
| 	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 { | ||||
| 	timeout time.Duration | ||||
| } | ||||
| @ -119,3 +127,35 @@ func DockerAllowNetworkAdministration(config *docker.HostConfig) { | ||||
| 		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" | ||||
| ) | ||||
| 
 | ||||
| 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 { | ||||
| 	suite.Suite | ||||
| 	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( | ||||
| 	tailscales map[string]dockertest.Resource, | ||||
| ) (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