mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-03-31 15:07:49 +01:00
feat: embedded derp
This commit is contained in:
@@ -6,9 +6,7 @@ import (
|
||||
"github.com/bufbuild/connect-go"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tailscale/hujson"
|
||||
"gopkg.in/yaml.v2"
|
||||
"os"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -19,8 +17,6 @@ func systemCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
command.AddCommand(getDefaultDERPMap())
|
||||
command.AddCommand(setDefaultDERPMap())
|
||||
command.AddCommand(resetDefaultDERPMap())
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -28,7 +24,7 @@ func systemCommand() *cobra.Command {
|
||||
func getDefaultDERPMap() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "get-derp-map",
|
||||
Short: "Get the DERP Map configuration",
|
||||
Short: "Get the default DERP Map configuration",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
@@ -72,63 +68,3 @@ func getDefaultDERPMap() *cobra.Command {
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func setDefaultDERPMap() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "set-derp-map",
|
||||
Short: "Set the DERP Map configuration",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
var file string
|
||||
|
||||
command.Flags().StringVar(&file, "file", "", "Path to json file with the DERP Map configuration")
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawJson, err := hujson.Standardize(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := tc.Client().SetDefaultDERPMap(cmd.Context(), connect.NewRequest(&api.SetDefaultDERPMapRequest{Value: rawJson}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var derpMap tailcfg.DERPMap
|
||||
if err := json.Unmarshal(resp.Msg.Value, &derpMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("DERP Map updated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func resetDefaultDERPMap() *cobra.Command {
|
||||
command, tc := prepareCommand(false, &cobra.Command{
|
||||
Use: "reset-derp-map",
|
||||
Short: "Reset the DERP Map to the default configuration",
|
||||
SilenceUsage: true,
|
||||
})
|
||||
|
||||
command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
if _, err := tc.Client().ResetDefaultDERPMap(cmd.Context(), connect.NewRequest(&api.ResetDefaultDERPMapRequest{})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("DERP Map updated successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"tailscale.com/tailcfg"
|
||||
tkey "tailscale.com/types/key"
|
||||
"time"
|
||||
)
|
||||
@@ -102,6 +103,7 @@ func defaultConfig() *Config {
|
||||
return &Config{
|
||||
WebListenAddr: ":8080",
|
||||
MetricsListenAddr: ":9091",
|
||||
StunListenAddr: ":3478",
|
||||
Database: Database{
|
||||
Type: "sqlite",
|
||||
Url: "./ionscale.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)",
|
||||
@@ -120,6 +122,14 @@ func defaultConfig() *Config {
|
||||
DNS: DNS{
|
||||
MagicDNSSuffix: defaultMagicDNSSuffix,
|
||||
},
|
||||
DERP: DERP{
|
||||
Server: DERPServer{
|
||||
Disabled: false,
|
||||
RegionID: 1000,
|
||||
RegionCode: "ionscale",
|
||||
RegionName: "ionscale Embedded DERP",
|
||||
},
|
||||
},
|
||||
Logging: Logging{
|
||||
Level: "info",
|
||||
},
|
||||
@@ -134,17 +144,25 @@ type ServerKeys struct {
|
||||
|
||||
type Config struct {
|
||||
WebListenAddr string `yaml:"web_listen_addr,omitempty" env:"WEB_LISTEN_ADDR"`
|
||||
StunListenAddr string `yaml:"stun_listen_addr,omitempty" env:"STUN_LISTEN_ADDR"`
|
||||
MetricsListenAddr string `yaml:"metrics_listen_addr,omitempty" env:"METRICS_LISTEN_ADDR"`
|
||||
WebPublicAddr string `yaml:"web_public_addr,omitempty" env:"WEB_PUBLIC_ADDR"`
|
||||
StunPublicAddr string `yaml:"stun_public_addr,omitempty" env:"STUN_PUBLIC_ADDR"`
|
||||
Tls Tls `yaml:"tls,omitempty" envPrefix:"TLS_"`
|
||||
PollNet PollNet `yaml:"poll_net,omitempty" envPrefix:"POLL_NET_"`
|
||||
Keys Keys `yaml:"keys,omitempty" envPrefix:"KEYS_"`
|
||||
Database Database `yaml:"database,omitempty" envPrefix:"DB_"`
|
||||
Auth Auth `yaml:"auth,omitempty" envPrefix:"AUTH_"`
|
||||
DNS DNS `yaml:"dns,omitempty"`
|
||||
DERP DERP `yaml:"derp,omitempty" envPrefix:"DERP_"`
|
||||
Logging Logging `yaml:"logging,omitempty" envPrefix:"LOGGING_"`
|
||||
|
||||
WebPublicUrl *url.URL `yaml:"-"`
|
||||
|
||||
stunHost string
|
||||
stunPort int
|
||||
derpHost string
|
||||
derpPort int
|
||||
}
|
||||
|
||||
type Tls struct {
|
||||
@@ -211,13 +229,38 @@ type SystemAdminPolicy struct {
|
||||
Filters []string `yaml:"filters,omitempty"`
|
||||
}
|
||||
|
||||
type DERP struct {
|
||||
Server DERPServer `yaml:"server,omitempty"`
|
||||
Sources []string `yaml:"sources,omitempty"`
|
||||
}
|
||||
|
||||
type DERPServer struct {
|
||||
Disabled bool `yaml:"disabled,omitempty"`
|
||||
RegionID int `yaml:"region_id,omitempty"`
|
||||
RegionCode string `yaml:"region_code,omitempty"`
|
||||
RegionName string `yaml:"region_name,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Config) Validate() (*Config, error) {
|
||||
publicWebUrl, err := publicAddrToUrl(c.WebPublicAddr)
|
||||
publicWebUrl, webHost, webPort, err := validatePublicAddr(c.WebPublicAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("web public addr: %w", err)
|
||||
}
|
||||
|
||||
c.WebPublicUrl = publicWebUrl
|
||||
c.derpHost = webHost
|
||||
c.derpPort = webPort
|
||||
|
||||
if !c.DERP.Server.Disabled {
|
||||
_, stunHost, stunPort, err := validatePublicAddr(c.StunPublicAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stun public addr: %w", err)
|
||||
}
|
||||
|
||||
c.stunHost = stunHost
|
||||
c.stunPort = stunPort
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -264,3 +307,52 @@ func (c *Config) ReadServerKeys(defaultKeys *domain.ControlKeys) (*ServerKeys, e
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (c *Config) DefaultDERPMap() *tailcfg.DERPMap {
|
||||
if c.derpHost == c.stunHost {
|
||||
return &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
c.DERP.Server.RegionID: {
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
RegionCode: c.DERP.Server.RegionCode,
|
||||
RegionName: c.DERP.Server.RegionName,
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
Name: "ionscale",
|
||||
HostName: c.derpHost,
|
||||
DERPPort: c.derpPort,
|
||||
STUNPort: c.stunPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
c.DERP.Server.RegionID: {
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
RegionCode: c.DERP.Server.RegionCode,
|
||||
RegionName: c.DERP.Server.RegionName,
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
Name: "stun",
|
||||
HostName: c.stunHost,
|
||||
STUNOnly: true,
|
||||
STUNPort: c.stunPort,
|
||||
},
|
||||
{
|
||||
RegionID: c.DERP.Server.RegionID,
|
||||
Name: "derp",
|
||||
HostName: c.derpHost,
|
||||
DERPPort: c.derpPort,
|
||||
STUNPort: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,7 @@ func GetString(key, defaultValue string) string {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func publicAddrToUrl(addr string) (*url.URL, error) {
|
||||
func validatePublicAddr(addr string) (*url.URL, string, int, error) {
|
||||
scheme := "https"
|
||||
|
||||
if strings.HasPrefix(addr, "http://") {
|
||||
@@ -37,14 +38,19 @@ func publicAddrToUrl(addr string) (*url.URL, error) {
|
||||
addr = strings.TrimPrefix(addr, "https://")
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
host, portS, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid public addr")
|
||||
return nil, "", -1, fmt.Errorf("invalid")
|
||||
}
|
||||
|
||||
if (port == "443" && scheme == "https") || (port == "80" && scheme == "http") || port == "" {
|
||||
return &url.URL{Scheme: scheme, Host: host}, nil
|
||||
port, err := strconv.Atoi(portS)
|
||||
if err != nil {
|
||||
return nil, "", 0, fmt.Errorf("invalid")
|
||||
}
|
||||
|
||||
return &url.URL{Scheme: scheme, Host: fmt.Sprintf("%s:%s", host, port)}, nil
|
||||
if (port == 443 && scheme == "https") || (port == 80 && scheme == "http") {
|
||||
return &url.URL{Scheme: scheme, Host: host}, host, port, nil
|
||||
}
|
||||
|
||||
return &url.URL{Scheme: scheme, Host: fmt.Sprintf("%s:%s", host, port)}, host, port, nil
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestPublicAddrToUrl(t *testing.T) {
|
||||
|
||||
for _, p := range parameters {
|
||||
t.Run(fmt.Sprintf("Testing [%v]", p.input), func(t *testing.T) {
|
||||
url, err := publicAddrToUrl(p.input)
|
||||
url, err := validatePublicAddr(p.input)
|
||||
require.Equal(t, p.expected, url)
|
||||
require.Equal(t, p.err, err)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package derp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"os"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func LoadDERPSources(c *config.Config) (*tailcfg.DERPMap, error) {
|
||||
derpMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{},
|
||||
}
|
||||
|
||||
var merr *multierror.Error
|
||||
for _, src := range c.DERP.Sources {
|
||||
dm, err := loadDERPSource(src)
|
||||
if err != nil {
|
||||
merr = multierror.Append(merr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for id, r := range dm.Regions {
|
||||
derpMap.Regions[id] = r
|
||||
}
|
||||
}
|
||||
|
||||
if !c.DERP.Server.Disabled {
|
||||
dm := c.DefaultDERPMap()
|
||||
for id, r := range dm.Regions {
|
||||
derpMap.Regions[id] = r
|
||||
}
|
||||
}
|
||||
|
||||
return derpMap, merr.ErrorOrNil()
|
||||
}
|
||||
|
||||
func loadDERPSource(src string) (*tailcfg.DERPMap, error) {
|
||||
temp, err := os.CreateTemp(os.TempDir(), "derp-*.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(temp.Name())
|
||||
|
||||
if err := getter.Get(temp.Name(), src, getter.WithMode(getter.ClientModeFile)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(temp.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dm tailcfg.DERPMap
|
||||
|
||||
if err := json.Unmarshal(content, &dm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dm, nil
|
||||
}
|
||||
@@ -5,16 +5,50 @@ import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"sync"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
_defaultDERPMapMu sync.RWMutex
|
||||
_defaultDERPMap = WrapDERPMap(tailcfg.DERPMap{})
|
||||
)
|
||||
|
||||
func SetDefaultDERPMap(v *tailcfg.DERPMap) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_defaultDERPMapMu.Lock()
|
||||
defer _defaultDERPMapMu.Unlock()
|
||||
_defaultDERPMap = WrapDERPMap(*v)
|
||||
}
|
||||
|
||||
func GetDefaultDERPMap() DERPMap {
|
||||
_defaultDERPMapMu.RLock()
|
||||
defer _defaultDERPMapMu.RUnlock()
|
||||
return _defaultDERPMap
|
||||
}
|
||||
|
||||
type DERPMap struct {
|
||||
Checksum string
|
||||
DERPMap tailcfg.DERPMap
|
||||
}
|
||||
|
||||
func (d DERPMap) GetDERPMap(_ context.Context) (*DERPMap, error) {
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func WrapDERPMap(d tailcfg.DERPMap) DERPMap {
|
||||
return DERPMap{
|
||||
Checksum: util.Checksum(d),
|
||||
DERPMap: d,
|
||||
}
|
||||
}
|
||||
|
||||
func (hi *DERPMap) Scan(destination interface{}) error {
|
||||
switch value := destination.(type) {
|
||||
case []byte:
|
||||
|
||||
@@ -2,14 +2,8 @@ package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"net/http"
|
||||
"sync"
|
||||
"tailscale.com/tailcfg"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
@@ -30,22 +24,17 @@ type Repository interface {
|
||||
GetJSONWebKeySet(ctx context.Context) (*JSONWebKeys, error)
|
||||
SetJSONWebKeySet(ctx context.Context, keys *JSONWebKeys) error
|
||||
|
||||
GetDERPMap(ctx context.Context) (*DERPMap, error)
|
||||
SetDERPMap(ctx context.Context, v *DERPMap) error
|
||||
|
||||
Transaction(func(rp Repository) error) error
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) Repository {
|
||||
return &repository{
|
||||
db: db,
|
||||
defaultDERPMap: &derpMapCache{},
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
db *gorm.DB
|
||||
defaultDERPMap *derpMapCache
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *repository) withContext(ctx context.Context) *gorm.DB {
|
||||
@@ -57,44 +46,3 @@ func (r *repository) Transaction(action func(Repository) error) error {
|
||||
return action(NewRepository(tx))
|
||||
})
|
||||
}
|
||||
|
||||
type derpMapCache struct {
|
||||
sync.RWMutex
|
||||
value *DERPMap
|
||||
}
|
||||
|
||||
func (d *derpMapCache) Get() (*DERPMap, error) {
|
||||
d.RLock()
|
||||
|
||||
if d.value != nil {
|
||||
d.RUnlock()
|
||||
return d.value, nil
|
||||
}
|
||||
d.RUnlock()
|
||||
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
|
||||
getJson := func(url string, target interface{}) error {
|
||||
c := http.Client{Timeout: 5 * time.Second}
|
||||
r, err := c.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return json.NewDecoder(r.Body).Decode(target)
|
||||
}
|
||||
|
||||
m := &tailcfg.DERPMap{}
|
||||
if err := getJson("https://controlplane.tailscale.com/derpmap/default", m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.value = &DERPMap{
|
||||
Checksum: util.Checksum(m),
|
||||
DERPMap: *m,
|
||||
}
|
||||
|
||||
return d.value, nil
|
||||
}
|
||||
|
||||
@@ -81,30 +81,6 @@ func (r *repository) SetJSONWebKeySet(ctx context.Context, v *JSONWebKeys) error
|
||||
return r.setServerConfig(ctx, jwksConfigKey, v)
|
||||
}
|
||||
|
||||
func (r *repository) GetDERPMap(ctx context.Context) (*DERPMap, error) {
|
||||
var m DERPMap
|
||||
|
||||
err := r.getServerConfig(ctx, derpMapConfigKey, &m)
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return r.defaultDERPMap.Get()
|
||||
}
|
||||
|
||||
if m.Checksum == "" {
|
||||
return r.defaultDERPMap.Get()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func (r *repository) SetDERPMap(ctx context.Context, v *DERPMap) error {
|
||||
return r.setServerConfig(ctx, "derp_map", v)
|
||||
}
|
||||
|
||||
func (r *repository) getServerConfig(ctx context.Context, s configKey, v interface{}) error {
|
||||
var m ServerConfig
|
||||
tx := r.withContext(ctx).Take(&m, "key = ?", s)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
func NewDERPHandler() *DERPHandlers {
|
||||
logger := zap.L().Named("derp")
|
||||
return &DERPHandlers{
|
||||
s: derp.NewServer(key.NewNode(), func(format string, args ...any) {
|
||||
logger.Debug(fmt.Sprintf(format, args...))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
type DERPHandlers struct {
|
||||
s *derp.Server
|
||||
}
|
||||
|
||||
func (h *DERPHandlers) Handler(c echo.Context) error {
|
||||
derphttp.Handler(h.s).ServeHTTP(c.Response(), c.Request())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DERPHandlers) LatencyCheck(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
func (h *DERPHandlers) DebugTraffic(c echo.Context) error {
|
||||
h.s.ServeDebugTraffic(c.Response(), c.Request())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DERPHandlers) DebugCheck(c echo.Context) error {
|
||||
if err := h.s.ConsistencyCheck(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.String(http.StatusOK, "DERP Server ConsistencyCheck okay")
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (h *PollNetMapper) CreateMapResponse(ctx context.Context, delta bool) (*Map
|
||||
return nil, err
|
||||
}
|
||||
|
||||
derpMap, err := m.Tailnet.GetDERPMap(ctx, h.repository)
|
||||
derpMap, err := m.Tailnet.GetDERPMap(ctx, domain.GetDefaultDERPMap())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ import (
|
||||
"github.com/jsiebens/ionscale/internal/config"
|
||||
"github.com/jsiebens/ionscale/internal/core"
|
||||
"github.com/jsiebens/ionscale/internal/database"
|
||||
"github.com/jsiebens/ionscale/internal/derp"
|
||||
"github.com/jsiebens/ionscale/internal/dns"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/handlers"
|
||||
"github.com/jsiebens/ionscale/internal/service"
|
||||
"github.com/jsiebens/ionscale/internal/stunserver"
|
||||
"github.com/jsiebens/ionscale/internal/templates"
|
||||
"github.com/labstack/echo-contrib/echoprometheus"
|
||||
"github.com/labstack/echo-contrib/pprof"
|
||||
@@ -51,6 +53,13 @@ func Start(ctx context.Context, c *config.Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
derpMap, err := derp.LoadDERPSources(c)
|
||||
if err != nil {
|
||||
logger.Warn("not all derp sources are read successfully", zap.Error(err))
|
||||
}
|
||||
|
||||
domain.SetDefaultDERPMap(derpMap)
|
||||
|
||||
httpLogger := logger.Named("http")
|
||||
dbLogger := logger.Named("db")
|
||||
|
||||
@@ -168,6 +177,16 @@ func Start(ctx context.Context, c *config.Config) error {
|
||||
webMux.GET("/a/success", authenticationHandlers.Success, csrf)
|
||||
webMux.GET("/a/error", authenticationHandlers.Error, csrf)
|
||||
|
||||
if !c.DERP.Server.Disabled {
|
||||
derpHandlers := handlers.NewDERPHandler()
|
||||
|
||||
metricsMux.GET("/debug/derp/traffic", derpHandlers.DebugTraffic)
|
||||
metricsMux.GET("/debug/derp/check", derpHandlers.DebugCheck)
|
||||
|
||||
webMux.GET("/derp", derpHandlers.Handler)
|
||||
webMux.GET("/derp/latency-check", derpHandlers.LatencyCheck)
|
||||
}
|
||||
|
||||
webL, err := webListener(c)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
@@ -178,6 +197,11 @@ func Start(ctx context.Context, c *config.Config) error {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
stunL, err := stunListener(c)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
errorLog, err := zap.NewStdLogAt(logger, zap.DebugLevel)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
@@ -185,6 +209,7 @@ func Start(ctx context.Context, c *config.Config) error {
|
||||
|
||||
webServer := &http.Server{ErrorLog: errorLog, Handler: h2c.NewHandler(webMux, &http2.Server{})}
|
||||
metricsServer := &http.Server{ErrorLog: errorLog, Handler: metricsMux}
|
||||
stunServer := stunserver.New(stunL)
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
@@ -193,20 +218,34 @@ func Start(ctx context.Context, c *config.Config) error {
|
||||
logger.Sugar().Infow("Shutting down ionscale server")
|
||||
shutdownHttpServer(metricsServer)
|
||||
shutdownHttpServer(webServer)
|
||||
_ = stunServer.Shutdown()
|
||||
}()
|
||||
|
||||
g.Go(func() error { return serveHttp(webServer, webL) })
|
||||
g.Go(func() error { return serveHttp(metricsServer, metricsL) })
|
||||
g.Go(func() error { return stunServer.Serve() })
|
||||
|
||||
fields := []zap.Field{
|
||||
zap.String("url", c.WebPublicUrl.String()),
|
||||
zap.String("addr", c.WebListenAddr),
|
||||
zap.String("metrics_addr", c.MetricsListenAddr),
|
||||
}
|
||||
|
||||
if !c.DERP.Server.Disabled {
|
||||
fields = append(fields, zap.String("stun_addr", c.StunListenAddr))
|
||||
} else {
|
||||
logger.Warn("Embedded DERP is disabled")
|
||||
}
|
||||
|
||||
if c.Tls.AcmeEnabled {
|
||||
logger.Sugar().Infow("TLS is enabled with ACME", "domain", c.WebPublicUrl.Hostname())
|
||||
logger.Sugar().Infow("Server is running", "addr", c.WebListenAddr, "metrics_addr", c.MetricsListenAddr, "url", c.WebPublicUrl)
|
||||
logger.Info("TLS is enabled with ACME", zap.String("domain", c.WebPublicUrl.Hostname()))
|
||||
logger.Info("Server is running", fields...)
|
||||
} else if !c.Tls.Disable {
|
||||
logger.Sugar().Infow("TLS is enabled", "cert", c.Tls.CertFile)
|
||||
logger.Sugar().Infow("Server is running", "addr", c.WebListenAddr, "metrics_addr", c.MetricsListenAddr, "url", c.WebPublicUrl)
|
||||
logger.Info("TLS is enabled", zap.String("cert", c.Tls.CertFile))
|
||||
logger.Info("Server is running", fields...)
|
||||
} else {
|
||||
logger.Sugar().Warnw("TLS is disabled")
|
||||
logger.Sugar().Infow("Server is running", "addr", c.WebListenAddr, "metrics_addr", c.MetricsListenAddr, "url", c.WebPublicUrl)
|
||||
logger.Warn("TLS is disabled")
|
||||
logger.Info("Server is running", fields...)
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
@@ -280,6 +319,19 @@ func metricsListener(config *config.Config) (net.Listener, error) {
|
||||
return net.Listen("tcp", config.MetricsListenAddr)
|
||||
}
|
||||
|
||||
func stunListener(config *config.Config) (*net.UDPConn, error) {
|
||||
if config.DERP.Server.Disabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", config.StunListenAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return net.ListenUDP("udp", addr)
|
||||
}
|
||||
|
||||
func setupLogging(config config.Logging) (*zap.Logger, error) {
|
||||
level, err := zap.ParseAtomicLevel(config.Level)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"github.com/jsiebens/ionscale/internal/domain"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (s *Service) GetDefaultDERPMap(ctx context.Context, _ *connect.Request[api.GetDefaultDERPMapRequest]) (*connect.Response[api.GetDefaultDERPMapResponse], error) {
|
||||
@@ -17,10 +15,7 @@ func (s *Service) GetDefaultDERPMap(ctx context.Context, _ *connect.Request[api.
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
||||
}
|
||||
|
||||
dm, err := s.repository.GetDERPMap(ctx)
|
||||
if err != nil {
|
||||
return nil, logError(err)
|
||||
}
|
||||
dm := domain.GetDefaultDERPMap()
|
||||
|
||||
raw, err := json.Marshal(dm.DERPMap)
|
||||
if err != nil {
|
||||
@@ -29,59 +24,3 @@ func (s *Service) GetDefaultDERPMap(ctx context.Context, _ *connect.Request[api.
|
||||
|
||||
return connect.NewResponse(&api.GetDefaultDERPMapResponse{Value: raw}), nil
|
||||
}
|
||||
|
||||
func (s *Service) SetDefaultDERPMap(ctx context.Context, req *connect.Request[api.SetDefaultDERPMapRequest]) (*connect.Response[api.SetDefaultDERPMapResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
||||
}
|
||||
|
||||
var derpMap tailcfg.DERPMap
|
||||
if err := json.Unmarshal(req.Msg.Value, &derpMap); err != nil {
|
||||
return nil, logError(err)
|
||||
}
|
||||
|
||||
dp := domain.DERPMap{
|
||||
Checksum: util.Checksum(&derpMap),
|
||||
DERPMap: derpMap,
|
||||
}
|
||||
|
||||
if err := s.repository.SetDERPMap(ctx, &dp); err != nil {
|
||||
return nil, logError(err)
|
||||
}
|
||||
|
||||
tailnets, err := s.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, logError(err)
|
||||
}
|
||||
|
||||
for _, t := range tailnets {
|
||||
s.sessionManager.NotifyAll(t.ID)
|
||||
}
|
||||
|
||||
return connect.NewResponse(&api.SetDefaultDERPMapResponse{Value: req.Msg.Value}), nil
|
||||
}
|
||||
|
||||
func (s *Service) ResetDefaultDERPMap(ctx context.Context, req *connect.Request[api.ResetDefaultDERPMapRequest]) (*connect.Response[api.ResetDefaultDERPMapResponse], error) {
|
||||
principal := CurrentPrincipal(ctx)
|
||||
if !principal.IsSystemAdmin() {
|
||||
return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied"))
|
||||
}
|
||||
|
||||
dp := domain.DERPMap{}
|
||||
|
||||
if err := s.repository.SetDERPMap(ctx, &dp); err != nil {
|
||||
return nil, logError(err)
|
||||
}
|
||||
|
||||
tailnets, err := s.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, logError(err)
|
||||
}
|
||||
|
||||
for _, t := range tailnets {
|
||||
s.sessionManager.NotifyAll(t.ID)
|
||||
}
|
||||
|
||||
return connect.NewResponse(&api.ResetDefaultDERPMapResponse{}), nil
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ func (s *Service) GetDERPMap(ctx context.Context, req *connect.Request[api.GetDE
|
||||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet not found"))
|
||||
}
|
||||
|
||||
derpMap, err := tailnet.GetDERPMap(ctx, s.repository)
|
||||
derpMap, err := tailnet.GetDERPMap(ctx, domain.GetDefaultDERPMap())
|
||||
if err != nil {
|
||||
return nil, logError(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package stunserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
)
|
||||
|
||||
var (
|
||||
stunRequests = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "ionscale",
|
||||
Name: "stun_requests",
|
||||
}, []string{"disposition"})
|
||||
|
||||
stunAddrFamily = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "ionscale",
|
||||
Name: "stun_addr_family",
|
||||
}, []string{"family"})
|
||||
|
||||
stunReadError = stunRequests.WithLabelValues("read_error")
|
||||
stunNotSTUN = stunRequests.WithLabelValues("not_stun")
|
||||
stunWriteError = stunRequests.WithLabelValues("write_error")
|
||||
stunSuccess = stunRequests.WithLabelValues("success")
|
||||
|
||||
stunIPv4 = stunAddrFamily.WithLabelValues("ipv4")
|
||||
stunIPv6 = stunAddrFamily.WithLabelValues("ipv6")
|
||||
)
|
||||
|
||||
type STUNServer struct {
|
||||
pc *net.UDPConn
|
||||
}
|
||||
|
||||
func New(pc *net.UDPConn) *STUNServer {
|
||||
return &STUNServer{pc: pc}
|
||||
}
|
||||
|
||||
func (s *STUNServer) Serve() error {
|
||||
if s.pc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var buf [64 << 10]byte
|
||||
var (
|
||||
n int
|
||||
ua *net.UDPAddr
|
||||
err error
|
||||
)
|
||||
for {
|
||||
n, ua, err = s.pc.ReadFromUDP(buf[:])
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
stunReadError.Inc()
|
||||
continue
|
||||
}
|
||||
pkt := buf[:n]
|
||||
if !stun.Is(pkt) {
|
||||
stunNotSTUN.Inc()
|
||||
continue
|
||||
}
|
||||
txid, err := stun.ParseBindingRequest(pkt)
|
||||
if err != nil {
|
||||
stunNotSTUN.Inc()
|
||||
continue
|
||||
}
|
||||
if ua.IP.To4() != nil {
|
||||
stunIPv4.Inc()
|
||||
} else {
|
||||
stunIPv6.Inc()
|
||||
}
|
||||
addr, _ := netip.AddrFromSlice(ua.IP)
|
||||
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(ua.Port)))
|
||||
_, err = s.pc.WriteTo(res, ua)
|
||||
if err != nil {
|
||||
stunWriteError.Inc()
|
||||
} else {
|
||||
stunSuccess.Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *STUNServer) Shutdown() error {
|
||||
if s.pc == nil {
|
||||
return nil
|
||||
}
|
||||
return s.pc.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user