From 0b1c9c28aedf2d17fe3e23dc662bdb7be7babc0a Mon Sep 17 00:00:00 2001 From: Alexander Baryshnikov Date: Tue, 7 Apr 2020 21:17:18 +0800 Subject: [PATCH] draft v2 --- cmd/tinc-web-boot/internal/support.go | 9 ++ cmd/tinc-web-boot/internal/support_windows.go | 41 +++++ cmd/tinc-web-boot/main.go | 33 ++++ go.mod | 6 +- go.sum | 47 ++++++ network/network.go | 98 ++++++++++- tincd/api.go | 98 +++++++++++ tincd/net.go | 132 +++++++++++++++ tincd/peer.go | 152 ++++++++++++++++++ tincd/pool.go | 107 ++++++++++++ utils/cmd_default.go | 9 ++ utils/cmd_linux.go | 13 ++ utils/flock.go | 79 +++++++++ 13 files changed, 819 insertions(+), 5 deletions(-) create mode 100644 cmd/tinc-web-boot/internal/support.go create mode 100644 cmd/tinc-web-boot/internal/support_windows.go create mode 100644 tincd/net.go create mode 100644 tincd/peer.go create mode 100644 tincd/pool.go create mode 100644 utils/cmd_default.go create mode 100644 utils/cmd_linux.go create mode 100644 utils/flock.go diff --git a/cmd/tinc-web-boot/internal/support.go b/cmd/tinc-web-boot/internal/support.go new file mode 100644 index 0000000..611d96d --- /dev/null +++ b/cmd/tinc-web-boot/internal/support.go @@ -0,0 +1,9 @@ +package internal + +import ( + "os/exec" +) + +func DetectTincBinary(possibleBinary string) (string, error) { + return exec.LookPath(possibleBinary) +} diff --git a/cmd/tinc-web-boot/internal/support_windows.go b/cmd/tinc-web-boot/internal/support_windows.go new file mode 100644 index 0000000..6bb2037 --- /dev/null +++ b/cmd/tinc-web-boot/internal/support_windows.go @@ -0,0 +1,41 @@ +package internal + +import ( + "os" + "os/exec" + "path/filepath" +) + +func DetectTincBinary(possibleBinary string) (string, error) { + if v, err := os.Stat(possibleBinary); err == nil && !v.IsDir() { + return possibleBinary, nil + } + // look near executable + execPath, err := os.Executable() + if err != nil { + return "", err + } + binary := digAround(filepath.Dir(execPath)) + if binary != "" { + return binary, nil + } + return exec.LookPath(possibleBinary) +} + +func digAround(dir string) string { + var ans string + _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if info.Name() == "tincd.exe" { + ans = filepath.Join(path, info.Name()) + return os.ErrExist + } + return nil + }) + return ans +} diff --git a/cmd/tinc-web-boot/main.go b/cmd/tinc-web-boot/main.go index 06ab7d0..7814340 100644 --- a/cmd/tinc-web-boot/main.go +++ b/cmd/tinc-web-boot/main.go @@ -1 +1,34 @@ package main + +import ( + "github.com/alecthomas/kong" + "log" + "tinc-web-boot/cmd/tinc-web-boot/internal" +) + +type Main struct { + APIPort int `name:"api-port" env:"API_PORT" help:"API port" default:"18655"` + TincBin string `name:"tinc-bin" env:"TINC_BIN" help:"Custom tinc binary location" default:"tincd"` + Host string `name:"host" env:"HOST" help:"Binding host" default:"127.0.0.1"` +} + +func main() { + var cli Main + ctx := kong.Parse(&cli) + var err error + if ctx.Command() == "" { + err = cli.Run() + } else { + err = ctx.Run(nil) + } + ctx.FatalIfErrorf(err) +} + +func (m *Main) Run() error { + binary, err := internal.DetectTincBinary(m.TincBin) + if err != nil { + return err + } + log.Println("Detected Tinc binary:", binary) + return nil +} diff --git a/go.mod b/go.mod index dd7fcfe..3712f48 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module tinc-web-boot go 1.13 -require github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect +require ( + github.com/alecthomas/kong v0.2.4 + github.com/gin-gonic/gin v1.6.2 // indirect + github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee +) diff --git a/go.sum b/go.sum index c15fed8..bddaa74 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,49 @@ +github.com/alecthomas/kong v0.2.4 h1:Y0ZBCHAvHhTHw7FFJ2FzCAAG4pkbTgA45nc7BpMhDNk= +github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.2 h1:88crIK23zO6TqlQBt+f9FrPJNKm9ZEr7qjp9vl/d5TM= +github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs= github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/network/network.go b/network/network.go index f8e7d0c..33fe0e4 100644 --- a/network/network.go +++ b/network/network.go @@ -1,11 +1,15 @@ package network import ( + "bytes" "context" + "fmt" "io/ioutil" "os" + "os/exec" "path/filepath" "regexp" + "tinc-web-boot/utils" ) type Storage struct { @@ -83,7 +87,7 @@ func (network *Network) Nodes() ([]string, error) { } func (network *Network) Node(name string) (*Node, error) { - data, err := ioutil.ReadFile(network.node(name)) + data, err := ioutil.ReadFile(network.NodeFile(name)) if err != nil { return nil, err } @@ -91,7 +95,20 @@ func (network *Network) Node(name string) (*Node, error) { return &nd, nd.UnmarshalText(data) } -func (network *Network) configure(ctx context.Context, apiPort int, tincBin string) error { +func (network *Network) Put(node *Node) error { + data, err := node.MarshalText() + if err != nil { + return err + } + return ioutil.WriteFile(network.NodeFile(node.Name), data, 0755) +} + +func (network *Network) IsDefined() bool { + v, err := os.Stat(network.configFile()) + return err == nil && !v.IsDir() +} + +func (network *Network) Configure(ctx context.Context, apiPort int, tincBin string) error { config, err := network.Read() if err != nil { return err @@ -116,9 +133,56 @@ func (network *Network) configure(ctx context.Context, apiPort int, tincBin stri if err := network.saveScript("subnet-down", subnetDown(selfExec, apiPort)); err != nil { return err } + + if err := network.generateKeysIfNeeded(ctx, tincBin); err != nil { + return fmt.Errorf("%s: generate keys: %w", network.Name(), err) + } + if err := network.indexPublicNodes(); err != nil { + return fmt.Errorf("%s: index public nodes: %w", network.Name(), err) + } return network.postConfigure(ctx, config, tincBin) } +func (network *Network) Logfile() string { + return filepath.Join(network.Root, "log.txt") +} + +func (network *Network) Pidfile() string { + return filepath.Join(network.Root, "pid.run") +} + +func (network *Network) Destroy() error { + return os.RemoveAll(network.Root) +} + +func (network *Network) indexPublicNodes() error { + config, err := network.Read() + if err != nil { + return err + } + + var publicNodes []string + + list, err := network.Nodes() + if err != nil { + return err + } + + for _, node := range list { + info, err := network.Node(node) + if err != nil { + return fmt.Errorf("parse node %s: %w", node, err) + } + if len(info.Address) > 0 { + publicNodes = append(publicNodes, node) + } + } + + config.ConnectTo = publicNodes + + return network.Update(config) +} + func (network *Network) configFile() string { return filepath.Join(network.Root, "tinc.conf") } @@ -127,7 +191,7 @@ func (network *Network) hosts() string { return filepath.Join(network.Root, "hosts") } -func (network *Network) node(name string) string { +func (network *Network) NodeFile(name string) string { name = regexp.MustCompile(`^[^a-zA-Z0-9_]+$`).ReplaceAllString(name, "") return filepath.Join(network.hosts(), name) } @@ -136,11 +200,37 @@ func (network *Network) scriptFile(name string) string { return filepath.Join(network.Root, name+scriptSuffix) } +func (network *Network) privateKeyFile() string { + return filepath.Join(network.Root, "rsa_key.priv") +} + func (network *Network) saveScript(name string, content string) error { file := network.scriptFile(name) err := ioutil.WriteFile(name, []byte(content), 0755) if err != nil { + return fmt.Errorf("%s: generate script %s: %w", network.Name(), name, err) + } + err = postProcessScript(file) + if err != nil { + return fmt.Errorf("%s: post-process script %s: %w", network.Name(), name, err) + } + return nil +} + +func (network *Network) generateKeysIfNeeded(ctx context.Context, tincBin string) error { + _, err := os.Stat(network.privateKeyFile()) + if err == nil { + return nil + } + if !os.IsNotExist(err) { return err } - return postProcessScript(file) + + cmd := exec.CommandContext(ctx, tincBin, "-K", "4096", "-c", network.Root) + cmd.Stdin = bytes.NewReader(nil) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + utils.SetCmdAttrs(cmd) + + return cmd.Run() } diff --git a/tincd/api.go b/tincd/api.go index f606dcf..d5f3da0 100644 --- a/tincd/api.go +++ b/tincd/api.go @@ -1 +1,99 @@ package tincd + +import ( + "context" + "github.com/gin-gonic/gin" + "log" + "net" + "net/http" + "strconv" + "strings" + "time" + "tinc-web-boot/network" +) + +const ( + retryInterval = 1 * time.Second + communicationPort = 1655 +) + +func runAPI(ctx context.Context, requests chan<- peerReq, network *network.Network) { + config, err := network.Read() + if err != nil { + log.Println(network.Name(), ": read config", err) + return + } + selfNode, err := network.Node(config.Name) + if err != nil { + log.Println(network.Name(), ": read self node", err) + return + } + bindingHost := strings.TrimSpace(strings.Split(selfNode.Subnet, "/")[0]) + + lc := &net.ListenConfig{} + var listener net.Listener + for { + + l, err := lc.Listen(ctx, "tcp", bindingHost+":"+strconv.Itoa(communicationPort)) + if err == nil { + listener = l + break + } + log.Println(network.Name(), "listen:", err) + select { + case <-ctx.Done(): + return + case <-time.After(retryInterval): + } + } + defer listener.Close() + router := setupRoutes(ctx, requests, network, config) + _ = router.RunListener(listener) +} + +func setupRoutes(ctx context.Context, requests chan<- peerReq, network *network.Network, config *network.Config) *gin.Engine { + router := gin.Default() + router.POST("/rpc/watch", func(gctx *gin.Context) { + var params struct { + Subnet string `json:"subnet"` + Node string `json:"node"` + } + if err := gctx.BindJSON(¶ms); err != nil { + return + } + select { + case requests <- peerReq{ + Node: params.Node, + Subnet: params.Subnet, + Add: true, + }: + case <-ctx.Done(): + + } + gctx.AbortWithStatus(http.StatusNoContent) + }) + + router.POST("/rpc/forget", func(gctx *gin.Context) { + var params struct { + Node string `json:"node"` + } + if err := gctx.BindJSON(¶ms); err != nil { + return + } + select { + case requests <- peerReq{ + Node: params.Node, + Add: false, + }: + case <-ctx.Done(): + + } + gctx.AbortWithStatus(http.StatusNoContent) + }) + + router.GET("/", func(gctx *gin.Context) { + gctx.File(network.NodeFile(config.Name)) + }) + + return router +} diff --git a/tincd/net.go b/tincd/net.go new file mode 100644 index 0000000..d937f0e --- /dev/null +++ b/tincd/net.go @@ -0,0 +1,132 @@ +package tincd + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "sync" + "tinc-web-boot/network" + "tinc-web-boot/utils" +) + +type netImpl struct { + apiPort int + tincBin string + ctx context.Context + + done chan struct{} + stop func() + lock sync.Mutex + peers peersManager + + definition *network.Network +} + +func (impl *netImpl) Start() { + impl.lock.Lock() + defer impl.lock.Unlock() + impl.unsafeStop() + + ctx, cancel := context.WithCancel(impl.ctx) + done := make(chan struct{}) + impl.stop = cancel + impl.done = done + impl.peers = peersManager{} + go func() { + defer cancel() + defer close(done) + err := impl.run(ctx) + if err != nil { + log.Println("failed run network", impl.definition.Name(), ":", err) + } + }() +} + +func (impl *netImpl) Stop() { + impl.lock.Lock() + defer impl.lock.Unlock() + impl.unsafeStop() +} + +func (impl *netImpl) Peers() []*Peer { + return impl.peers.List() +} + +func (impl *netImpl) Definition() *network.Network { + return impl.definition +} + +func (impl *netImpl) IsRunning() bool { + ch := impl.done + if ch == nil { + return false + } + select { + case <-ch: + return false + default: + return true + } +} + +func (impl *netImpl) unsafeStop() { + v := impl.stop + if v == nil { + return + } + v() + <-impl.done + impl.stop = nil +} + +func (impl *netImpl) run(global context.Context) error { + if err := impl.definition.Configure(global, impl.apiPort, impl.tincBin); err != nil { + return fmt.Errorf("configure: %w", err) + } + + ctx, abort := context.WithCancel(global) + defer abort() + + cmd := exec.CommandContext(ctx, impl.tincBin, "-D", "-d", "-d", "-d", + "--pidfile", impl.definition.Pidfile(), + "--logfile", impl.definition.Logfile(), + "-c", ".") + cmd.Dir = impl.definition.Root + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + utils.SetCmdAttrs(cmd) + + peers := make(chan peerReq) + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + defer abort() + err := cmd.Run() + if err != nil { + log.Println(impl.definition.Name(), "failed to run tinc:", err) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer abort() + runAPI(ctx, peers, impl.definition) + log.Println(impl.definition.Name(), "api stopped") + }() + + wg.Add(1) + go func() { + defer wg.Done() + defer abort() + impl.peers.Run(ctx, peers) + }() + + wg.Wait() + return ctx.Err() +} diff --git a/tincd/peer.go b/tincd/peer.go new file mode 100644 index 0000000..a6399f0 --- /dev/null +++ b/tincd/peer.go @@ -0,0 +1,152 @@ +package tincd + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "net/http" + "sort" + "strconv" + "strings" + "sync" + "time" + "tinc-web-boot/network" +) + +type peerReq struct { + Node string + Subnet string + Add bool +} + +type peersManager struct { + lock sync.RWMutex + network *network.Network + list map[string]*Peer +} + +func (pl *peersManager) Run(ctx context.Context, peers <-chan peerReq) { + var wg sync.WaitGroup +LOOP: + for { + select { + case <-ctx.Done(): + break LOOP + case req := <-peers: + if req.Add { + peer := pl.Add(ctx, req.Node, req.Subnet) + wg.Add(1) + go func() { + defer wg.Done() + peer.run(ctx) + }() + } else { + pl.Remove(req.Node) + + } + } + } + wg.Wait() +} + +func (pl *peersManager) Add(ctx context.Context, node, subnet string) *Peer { + ctx, cancel := context.WithCancel(ctx) + p := &Peer{ + Node: node, + Subnet: subnet, + stop: cancel, + network: pl.network, + } + pl.add(p) + return p +} + +func (pl *peersManager) Remove(node string) { + v, ok := pl.remove(node) + if ok { + v.stop() + } +} + +func (pl *peersManager) add(newPeer *Peer) { + pl.lock.Lock() + defer pl.lock.Unlock() + if pl.list == nil { + pl.list = make(map[string]*Peer) + } + pl.list[newPeer.Node] = newPeer +} + +func (pl *peersManager) remove(name string) (*Peer, bool) { + pl.lock.Lock() + defer pl.lock.Unlock() + v, ok := pl.list[name] + delete(pl.list, name) + return v, ok +} + +func (pl *peersManager) List() []*Peer { + pl.lock.RLock() + var cp = make([]*Peer, 0, len(pl.list)) + for _, v := range pl.list { + cp = append(cp, v) + } + pl.lock.RUnlock() + sort.Slice(cp, func(i, j int) bool { + return cp[i].Node < cp[j].Node + }) + return cp +} + +type Peer struct { + Node string + Subnet string + Fetched bool + stop func() + network *network.Network +} + +func (peer *Peer) run(ctx context.Context) { + for { + node, err := peer.fetchConfig(ctx) + if err == nil { + err = peer.network.Put(node) + } + if err == nil { + break + } + log.Println("failed get", peer.Node, ":", err) + select { + case <-ctx.Done(): + return + case <-time.After(retryInterval): + } + } + peer.Fetched = true +} + +func (peer *Peer) fetchConfig(ctx context.Context) (*network.Node, error) { + addr := strings.TrimSpace(strings.Split(peer.Subnet, "/")[0]) + url := "http://" + addr + ":" + strconv.Itoa(communicationPort) + "/" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%d %s", res.StatusCode, res.Status) + } + + data, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var node network.Node + return &node, node.UnmarshalText(data) +} diff --git a/tincd/pool.go b/tincd/pool.go new file mode 100644 index 0000000..f421552 --- /dev/null +++ b/tincd/pool.go @@ -0,0 +1,107 @@ +package tincd + +import ( + "context" + "fmt" + "sort" + "sync" + "tinc-web-boot/network" +) + +func New(ctx context.Context, storage *network.Storage, apiPort int, tincBin string) *poolImpl { + return &poolImpl{ + ctx: ctx, + apiPort: apiPort, + tincBin: tincBin, + storage: storage, + } +} + +type poolImpl struct { + apiPort int + tincBin string + + lock sync.Mutex + ctx context.Context + nets map[string]*netImpl + + storage *network.Storage +} + +func (pool *poolImpl) Get(name string) (*netImpl, error) { + nw := pool.storage.Get(name) + if !nw.IsDefined() { + return nil, fmt.Errorf("network %s is not defined", name) + } + v, _ := pool.ensure(nw) + return v, nil +} + +func (pool *poolImpl) Create(name string) (*netImpl, error) { + v, created := pool.ensure(pool.storage.Get(name)) + if created { + return v, v.definition.Configure(pool.ctx, pool.apiPort, pool.tincBin) + } + return v, nil +} + +func (pool *poolImpl) Remove(name string) error { + pool.lock.Lock() + defer pool.lock.Unlock() + + v, ok := pool.nets[name] + delete(pool.nets, name) + + if ok { + v.Stop() + return v.definition.Destroy() + } + return nil +} + +func (pool *poolImpl) Nets() []*netImpl { + pool.lock.Lock() + var ans = make([]*netImpl, 0, len(pool.nets)) + for _, v := range pool.nets { + ans = append(ans, v) + } + pool.lock.Unlock() + sort.Slice(ans, func(i, j int) bool { + return ans[i].definition.Name() < ans[j].definition.Name() + }) + return ans +} + +func (pool *poolImpl) Stop() { + var wg sync.WaitGroup + + for _, impl := range pool.Nets() { + wg.Add(1) + go func(impl *netImpl) { + defer wg.Done() + impl.Stop() + }(impl) + } + + wg.Wait() +} + +func (pool *poolImpl) ensure(netw *network.Network) (*netImpl, bool) { + pool.lock.Lock() + defer pool.lock.Unlock() + if pool.nets == nil { + pool.nets = make(map[string]*netImpl) + } + + if v, ok := pool.nets[netw.Name()]; ok { + return v, false + } + v := &netImpl{ + ctx: pool.ctx, + definition: netw, + tincBin: pool.tincBin, + apiPort: pool.apiPort, + } + pool.nets[netw.Name()] = v + return v, true +} diff --git a/utils/cmd_default.go b/utils/cmd_default.go new file mode 100644 index 0000000..e3d4f32 --- /dev/null +++ b/utils/cmd_default.go @@ -0,0 +1,9 @@ +// + +// +build !linux + +package utils + +import "os/exec" + +func SetCmdAttrs(cmd *exec.Cmd) {} diff --git a/utils/cmd_linux.go b/utils/cmd_linux.go new file mode 100644 index 0000000..efe8fcb --- /dev/null +++ b/utils/cmd_linux.go @@ -0,0 +1,13 @@ +package utils + +import ( + "os/exec" + "syscall" +) + +func SetCmdAttrs(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGKILL, + Setpgid: true, + } +} diff --git a/utils/flock.go b/utils/flock.go new file mode 100644 index 0000000..531eac3 --- /dev/null +++ b/utils/flock.go @@ -0,0 +1,79 @@ +package utils + +import ( + "context" + "sort" + "sync" +) + +type Runner interface { + Run(ctx context.Context) +} + +type runnerWrapper struct { + stop func() + done chan struct{} +} + +type Flock struct { + runners map[string]*runnerWrapper + lock sync.RWMutex + pool sync.WaitGroup +} + +func (fl *Flock) Add(ctx context.Context, name string, run Runner) { + fl.lock.Lock() + defer fl.lock.Unlock() + if fl.runners == nil { + fl.runners = make(map[string]*runnerWrapper) + } + old, ok := fl.runners[name] + if ok { + old.stop() + } + child, cancel := context.WithCancel(ctx) + wrap := &runnerWrapper{ + stop: cancel, + done: make(chan struct{}), + } + fl.runners[name] = wrap + fl.pool.Add(1) + go func() { + defer fl.pool.Done() + defer cancel() + defer close(wrap.done) + run.Run(child) + fl.lock.Lock() + delete(fl.runners, name) + fl.lock.Unlock() + }() +} + +func (fl *Flock) Remove(name string) <-chan struct{} { + fl.lock.Lock() + v, ok := fl.runners[name] + delete(fl.runners, name) + fl.lock.Unlock() + if ok { + v.stop() + return v.done + } + ch := make(chan struct{}) + close(ch) + return ch +} + +func (fl *Flock) WaitAll() { + fl.pool.Wait() +} + +func (fl *Flock) List() []string { + fl.lock.RLock() + var ans = make([]string, 0, len(fl.runners)) + for k := range fl.runners { + ans = append(ans, k) + } + fl.lock.RUnlock() + sort.Strings(ans) + return ans +}