This commit is contained in:
commit
1d7c7fba86
33
.gitea/workflows/release.yaml
Normal file
33
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install syft
|
||||||
|
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Golang
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.20.5'
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: https://github.com/goreleaser/goreleaser-action@v4
|
||||||
|
with:
|
||||||
|
distribution: goreleaser
|
||||||
|
version: latest
|
||||||
|
args: release --clean
|
||||||
|
env:
|
||||||
|
GORELEASER_FORCE_TOKEN: 'gitea'
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
ding
|
||||||
|
!ding/
|
||||||
|
dist/
|
29
.goreleaser.yaml
Normal file
29
.goreleaser.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
builds:
|
||||||
|
- main: ./cmd/ding/
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X gitea.illuad.fr/adrien/ding/cli.version={{.Version}} -X gitea.illuad.fr/adrien/ding/cli.commit={{.Commit}}
|
||||||
|
buildmode: pie
|
||||||
|
no_unique_dist_dir: true
|
||||||
|
archives:
|
||||||
|
- format: binary
|
||||||
|
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}"
|
||||||
|
checksum:
|
||||||
|
name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt"
|
||||||
|
algorithm: sha512
|
||||||
|
extra_files:
|
||||||
|
- glob: ./LICENSE
|
||||||
|
- glob: ./README.md
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
sboms:
|
||||||
|
- artifacts: archive
|
||||||
|
gitea_urls:
|
||||||
|
api: https://gitea.illuad.fr/api/v1
|
||||||
|
download: https://gitea.illuad.fr
|
||||||
|
skip_tls_verify: false
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Adrien
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
24
Makefile
Normal file
24
Makefile
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.SILENT: ding clean update check docker release
|
||||||
|
.PHONY: ding clean update check docker release
|
||||||
|
|
||||||
|
ding:
|
||||||
|
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -buildmode=pie -o ding cmd/ding/main.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm --force ding
|
||||||
|
|
||||||
|
update:
|
||||||
|
go get -u ./...
|
||||||
|
go mod tidy -v
|
||||||
|
|
||||||
|
check:
|
||||||
|
goreportcard-cli -v
|
||||||
|
govulncheck ./...
|
||||||
|
GOOS=linux staticcheck -f stylish ./...
|
||||||
|
|
||||||
|
docker:
|
||||||
|
docker build --build-arg VERSION=untagged --build-arg COMMIT=0000000000 -t ding:untagged .
|
||||||
|
docker run --rm --read-only --cap-drop=all --name ding ding:untagged --help
|
||||||
|
|
||||||
|
release:
|
||||||
|
goreleaser release --clean
|
130
README.md
Normal file
130
README.md
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# ding
|
||||||
|
|
||||||
|
ding pronounced `[diŋ]`, in the French language is an onomatopoeia evoking the sound produced by the bells of a steeple
|
||||||
|
or the bell of a front door. ding is a tool for port knocking, hence the name. It took me 10 seconds to find it, be
|
||||||
|
nice.
|
||||||
|
|
||||||
|
For those who haven't heard, port knocking is a method of externally opening ports on a firewall by generating a
|
||||||
|
connection attempt on a set of prespecified closed ports. Once a correct sequence of connection attempts is received,
|
||||||
|
the firewall rules are dynamically modified to allow the host which sent the connection attempts to connect over
|
||||||
|
specific port(s).
|
||||||
|
|
||||||
|
ding, your brand-new secure* port knocking client in less than 400 lines of code.
|
||||||
|
|
||||||
|
*In its default configuration, ding protects the configuration file by ciphering it via XChaCha20-Poly1305, an
|
||||||
|
authenticated encryption with additional data (AEAD) algorithm, that combines the XChaCha20 stream cipher with the
|
||||||
|
Poly1305 message authentication code.
|
||||||
|
|
||||||
|
## How to use it
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
The values of the `-t`, `--timeout` or `timeout` and `-d` `--delay` or `delay` flags are of
|
||||||
|
type [time.Duration](https://pkg.go.dev/time#Duration), which means that the time unit can take on the following
|
||||||
|
values: `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. Respectively: nanosecond, microsecond, millisecond, second, minute
|
||||||
|
and hour.
|
||||||
|
|
||||||
|
By default, and for obvious reasons, the configuration file is ciphered (via XChaCha20-Poly1305). You can disable this
|
||||||
|
behavior with the `-i` or `--insecure` flag.
|
||||||
|
|
||||||
|
Also, the minimum entropy of the password must be 65, you can (at your own risk) easily get around this by using
|
||||||
|
the `-b` or `--bypass-password-entropy` flag. Note that entropy is only checked during the setup phase.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ding setup --help
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
NAME:
|
||||||
|
ding setup - Launches ding setup
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
ding setup [command options] [arguments...]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--address value, -a value address to knock
|
||||||
|
--port value, -p value [ --port value, -p value ] ports to knock
|
||||||
|
--timeout value, -t value timeout in milliseconds (default: 1500ms)
|
||||||
|
--delay value, -d value delay in milliseconds between knocks (default: 100ms)
|
||||||
|
--insecure, -i don't de/cipher configuration file (default: false)
|
||||||
|
--bypass-password-entropy, -b insecurely bypass password entropy (default: false)
|
||||||
|
--help, -h show help
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Interactive mode
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ding setup
|
||||||
|
? address to knock: 192.168.10.6
|
||||||
|
? port to knock (separated by commas if several): 38457,22949,9686
|
||||||
|
? timeout in milliseconds: 1.5s
|
||||||
|
? delay in milliseconds between knocks: 100ms
|
||||||
|
? password: *****************
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Non-interactive mode
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ding setup -a 192.168.10.6 -p 38457 -p 22949 -p 9686 -t 1500ms -d 100ms
|
||||||
|
? password: *****************
|
||||||
|
```
|
||||||
|
|
||||||
|
These two approaches boil down to exactly the same thing.
|
||||||
|
|
||||||
|
If you go to `$XDG_CONFIG_HOME/ding/` or `$HOME/.config/ding/`, you'll find a file named `.salt` containing the salt
|
||||||
|
used to derive the 32-byte key used to cipher the configuration file (if you haven't used the `-i` or `--insecure`
|
||||||
|
flag), as well as the configuration file itself, ciphered or not.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ls -lah ~/.config/ding/
|
||||||
|
total 16K
|
||||||
|
drwxr-xr-x 2 adrien users 4.0K Jun 30 17:01 ./
|
||||||
|
drwxr-xr-x 30 adrien users 4.0K Jun 30 17:01 ../
|
||||||
|
-rw-r--r-- 1 adrien users 132 Jun 30 17:11 config.toml
|
||||||
|
-rw-r--r-- 1 adrien users 32 Jun 30 17:11 .salt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ding help
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
NAME:
|
||||||
|
ding - Command line interface tool to knock ports
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
ding [global options] command [command options] [arguments...]
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
untagged-0000000000
|
||||||
|
|
||||||
|
AUTHOR:
|
||||||
|
Adrien <contact@illuad.fr>
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
setup, s Launches ding setup
|
||||||
|
help, h Shows a list of commands or help for one command
|
||||||
|
|
||||||
|
GLOBAL OPTIONS:
|
||||||
|
--insecure, -i don't de/cipher configuration file (default: false)
|
||||||
|
--help, -h show help
|
||||||
|
--version, -v print the version
|
||||||
|
```
|
||||||
|
|
||||||
|
It couldn't be simpler. The password is the same as the one entered during the setup phase.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ding
|
||||||
|
? password: *****************
|
||||||
|
```
|
||||||
|
|
||||||
|
If you add the `-i` or `--insecure` flag when you haven't specified it during the setup step, you'll get an error like
|
||||||
|
this.
|
||||||
|
|
||||||
|
```
|
||||||
|
2023-07-01T11:09:51+02:00 FTL toml: line 1: invalid UTF-8 byte: 0xc4
|
||||||
|
```
|
||||||
|
|
||||||
|
However, if you've set up ding correctly, you should be able to access your server via SSH.
|
486
cli/cli.go
Normal file
486
cli/cli.go
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
// Package cli defines flags, commands and functions to run.
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
entropy "github.com/wagslane/go-password-validator"
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// config represents the configuration file
|
||||||
|
type config struct {
|
||||||
|
Address string `toml:"address"`
|
||||||
|
Port []string `toml:"port"`
|
||||||
|
Timeout time.Duration `toml:"timeout"`
|
||||||
|
Delay time.Duration `toml:"delay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ding holds variables to operate
|
||||||
|
type ding struct {
|
||||||
|
cliCtx *cli.Context
|
||||||
|
config
|
||||||
|
configFile *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
appName = "ding"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// version and commit can be defined at compilation time via ldflags.
|
||||||
|
version, commit string
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run() error {
|
||||||
|
// Use all processors
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
|
||||||
|
// Set global logger
|
||||||
|
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).With().Timestamp().Logger()
|
||||||
|
|
||||||
|
// Common flag
|
||||||
|
var noCipher = &cli.BoolFlag{Name: "insecure", Aliases: []string{"i"}, Usage: "don't de/cipher configuration file", Value: false}
|
||||||
|
|
||||||
|
// Initialize ding
|
||||||
|
app := cli.NewApp()
|
||||||
|
|
||||||
|
globalFlags := []cli.Flag{
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "address", Aliases: []string{"a"}, Usage: "address to knock", Hidden: true}),
|
||||||
|
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "port", Aliases: []string{"p"}, Usage: "ports to knock", Hidden: true, Action: func(_ *cli.Context, ports []string) error { return validatePortRange(ports) }}),
|
||||||
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "timeout", Aliases: []string{"t"}, Usage: "timeout in milliseconds", Hidden: true, DefaultText: "1500ms", Value: 1500 * time.Millisecond}),
|
||||||
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "delay", Aliases: []string{"d"}, Usage: "delay in milliseconds between knocks", Hidden: true, DefaultText: "100ms", Value: 100 * time.Millisecond}),
|
||||||
|
noCipher,
|
||||||
|
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "input configuration file", Hidden: true, DefaultText: "$XDG_CONFIG_HOME/ding/config.toml or $HOME/.config/ding/config.toml"},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Name = appName
|
||||||
|
if version == "" || commit == "" {
|
||||||
|
version = "untagged"
|
||||||
|
commit = "0000000000"
|
||||||
|
}
|
||||||
|
app.Version = version + "-" + commit[0:10]
|
||||||
|
app.Usage = "Command line interface tool to knock ports"
|
||||||
|
app.UseShortOptionHandling = true
|
||||||
|
app.Suggest = true
|
||||||
|
app.EnableBashCompletion = true
|
||||||
|
app.Authors = []*cli.Author{
|
||||||
|
{
|
||||||
|
Name: "Adrien",
|
||||||
|
Email: "contact@illuad.fr",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Flags = globalFlags
|
||||||
|
app.Commands = cli.Commands{
|
||||||
|
{
|
||||||
|
Name: "setup",
|
||||||
|
Aliases: []string{"s"},
|
||||||
|
Usage: "Launches ding setup",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{Name: "address", Aliases: []string{"a"}, Usage: "address to knock"},
|
||||||
|
&cli.StringSliceFlag{Name: "port", Aliases: []string{"p"}, Usage: "ports to knock", Action: func(_ *cli.Context, ports []string) error { return validatePortRange(ports) }},
|
||||||
|
&cli.DurationFlag{Name: "timeout", Aliases: []string{"t"}, Usage: "timeout in milliseconds", DefaultText: "1500ms", Value: 1500 * time.Millisecond},
|
||||||
|
&cli.DurationFlag{Name: "delay", Aliases: []string{"d"}, Usage: "delay in milliseconds between knocks", DefaultText: "100ms", Value: 100 * time.Millisecond},
|
||||||
|
noCipher,
|
||||||
|
&cli.BoolFlag{Name: "bypass-password-entropy", Aliases: []string{"b"}, Usage: "insecurely bypass password entropy", Value: false}},
|
||||||
|
Action: func(cliCtx *cli.Context) error {
|
||||||
|
dingCLI := ding{cliCtx: cliCtx}
|
||||||
|
|
||||||
|
dingCLI.
|
||||||
|
askAddressIfNotSet().
|
||||||
|
askPortIfNotSet().
|
||||||
|
askTimeoutIfNotSet().
|
||||||
|
askDelayIfNotSet()
|
||||||
|
|
||||||
|
if cliCtx.Bool("insecure") {
|
||||||
|
return dingCLI.write()
|
||||||
|
}
|
||||||
|
|
||||||
|
return dingCLI.marshal()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Before = func(cliCtx *cli.Context) error {
|
||||||
|
if cliCtx.IsSet("config") {
|
||||||
|
return altsrc.InitInputSourceWithContext(cliCtx.App.Flags, altsrc.NewTomlSourceFromFlagFunc("config"))(cliCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
app.Action = func(cliCtx *cli.Context) error {
|
||||||
|
dingCLI := ding{cliCtx: cliCtx}
|
||||||
|
|
||||||
|
if cliCtx.Bool("insecure") {
|
||||||
|
if err := dingCLI.read(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := dingCLI.unmarshal(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
address := dingCLI.config.Address
|
||||||
|
|
||||||
|
if _, err := net.LookupHost(address); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range dingCLI.config.Port {
|
||||||
|
_, _ = net.DialTimeout("tcp", fmt.Sprintf("%s:%s", address, p), dingCLI.config.Timeout)
|
||||||
|
|
||||||
|
time.Sleep(dingCLI.config.Delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// basePath returns configuration path, e.g. /home/jdoe/.config/ding.
|
||||||
|
// Configuration filename (config.toml) is not added, see configPath for this.
|
||||||
|
// Empty string is returned if path can't be build (if $XDG_CONFIG_HOME or $HOME are not set).
|
||||||
|
func (d *ding) basePath() string {
|
||||||
|
path, ok := os.LookupEnv("XDG_CONFIG_HOME")
|
||||||
|
if !ok {
|
||||||
|
// $XDG_CONFIG_HOME is not set
|
||||||
|
// $HOME is?
|
||||||
|
path, ok = os.LookupEnv("HOME")
|
||||||
|
if !ok {
|
||||||
|
// $HOME is not set
|
||||||
|
// Base path can't be build
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
// $HOME is set, add ".config/ding"
|
||||||
|
return filepath.Join(path, ".config", "ding")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// $XDG_CONFIG_HOME is set, add "ding"
|
||||||
|
return filepath.Join(path, "ding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// configPath return configuration file path, e.g. /home/jdoe/.config/ding/config.toml.
|
||||||
|
// Empty string is returned if configuration file path can't be build (if basePath has returned empty string).
|
||||||
|
func (d *ding) configPath() string {
|
||||||
|
path := d.basePath()
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(path, "config.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// saltPath return salt file path, e.g. /home/jdoe/.config/ding/.salt.
|
||||||
|
// Empty string is returned if salt file path can't be build (if basePath has returned empty string).
|
||||||
|
func (d *ding) saltPath() string {
|
||||||
|
path := d.basePath()
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(path, ".salt")
|
||||||
|
}
|
||||||
|
|
||||||
|
// createBaseFolders creates configuration path, e.g. /home/jdoe/.config/ding.
|
||||||
|
// Configuration file (config.toml) is not created, see createConfigurationFile for this.
|
||||||
|
// An error is returned if basePath has returned empty string.
|
||||||
|
func (d *ding) createBaseFolders() (string, error) {
|
||||||
|
path := d.basePath()
|
||||||
|
if path == "" {
|
||||||
|
return "", errors.New("failed to retrieve base path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, os.MkdirAll(path, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createConfigurationFile creates configuration file, e.g. /home/jdoe/.config/ding/config.toml.
|
||||||
|
// An error is return if createBaseFolders failed to create base folders or if configuration file creation failed.
|
||||||
|
func (d *ding) createConfigurationFile() error {
|
||||||
|
path, err := d.createBaseFolders()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.configFile, err = os.Create(filepath.Join(path, "config.toml"))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// askAddressIfNotSet asks for the address, or sets it if given by flag.
|
||||||
|
func (d *ding) askAddressIfNotSet() *ding {
|
||||||
|
if d.cliCtx.IsSet("address") {
|
||||||
|
d.config.Address = d.cliCtx.String("address")
|
||||||
|
} else {
|
||||||
|
if err := survey.AskOne(&survey.Input{
|
||||||
|
Message: "address to knock:",
|
||||||
|
}, &d.config.Address); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// askPortIfNotSet asks for the port, or sets it if given by flag.
|
||||||
|
func (d *ding) askPortIfNotSet() *ding {
|
||||||
|
if d.cliCtx.IsSet("port") {
|
||||||
|
d.config.Port = d.cliCtx.StringSlice("port")
|
||||||
|
} else {
|
||||||
|
var input string
|
||||||
|
|
||||||
|
if err := survey.AskOne(&survey.Input{
|
||||||
|
Message: "port to knock (separated by commas if several):",
|
||||||
|
}, &input); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.config.Port = strings.Split(input, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// askTimeoutIfNotSet asks for the timeout, or sets it if given by flag.
|
||||||
|
func (d *ding) askTimeoutIfNotSet() *ding {
|
||||||
|
if d.cliCtx.IsSet("timeout") {
|
||||||
|
d.config.Timeout = d.cliCtx.Duration("timeout")
|
||||||
|
} else {
|
||||||
|
if err := survey.AskOne(&survey.Input{
|
||||||
|
Message: "timeout in milliseconds:",
|
||||||
|
Default: d.cliCtx.Duration("timeout").String(),
|
||||||
|
Help: "TCP connection timeout, this value should be increased on a slow network",
|
||||||
|
}, &d.config.Timeout); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// askDelayIfNotSet asks for the delay, or sets it if given by flag.
|
||||||
|
func (d *ding) askDelayIfNotSet() *ding {
|
||||||
|
if d.cliCtx.IsSet("delay") {
|
||||||
|
d.config.Delay = d.cliCtx.Duration("delay")
|
||||||
|
} else {
|
||||||
|
if err := survey.AskOne(&survey.Input{
|
||||||
|
Message: "delay in milliseconds between knocks:",
|
||||||
|
Default: d.cliCtx.Duration("delay").String(),
|
||||||
|
Help: "waiting time between TCP connections, value tied to server configuration",
|
||||||
|
}, &d.config.Delay); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// write creates configuration file and encodes config to it.
|
||||||
|
func (d *ding) write() error {
|
||||||
|
if err := d.createConfigurationFile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := toml.NewEncoder(d.configFile).Encode(d.config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.configFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshal encodes config to a buffer of bytes suitable for post-manipulations.
|
||||||
|
// It also generates a 128-bit salt, a 256-bit key derived via Argon2id, initializes a XChaCha20-Poly1305 AEAD cipher.
|
||||||
|
// Then, the buffer is ciphered and written to the configuration file.
|
||||||
|
func (d *ding) marshal() error {
|
||||||
|
if err := d.createConfigurationFile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dst bytes.Buffer
|
||||||
|
|
||||||
|
if err := toml.NewEncoder(&dst).Encode(d.config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
salt := make([]byte, 16)
|
||||||
|
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path := d.saltPath()
|
||||||
|
if path == "" {
|
||||||
|
return errors.New("failed to retrieve salt path")
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedSalt := make([]byte, hex.EncodedLen(len(salt)))
|
||||||
|
|
||||||
|
hex.Encode(encodedSalt, salt)
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, encodedSalt, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
aead, err := chacha20poly1305.NewX(d.deriveKey(salt))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+dst.Len()+aead.Overhead())
|
||||||
|
|
||||||
|
if _, err = rand.Read(nonce); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = d.configFile.Write(aead.Seal(nonce, nonce, dst.Bytes(), nil)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.configFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ding) askPassword() *bytes.Buffer {
|
||||||
|
var input string
|
||||||
|
|
||||||
|
if err := survey.AskOne(&survey.Password{Message: "password:"}, &input); err != nil {
|
||||||
|
panic("prompt error:" + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.NewBufferString(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ding) deriveKey(salt []byte) []byte {
|
||||||
|
var password *bytes.Buffer
|
||||||
|
|
||||||
|
for {
|
||||||
|
password = d.askPassword()
|
||||||
|
|
||||||
|
// Password entropy must be verified only during the setup phase
|
||||||
|
if d.cliCtx.Command.Name != "setup" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.cliCtx.Bool("bypass-password-entropy") {
|
||||||
|
if err := entropy.Validate(password.String(), 65); err != nil {
|
||||||
|
log.Warn().Msg(err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
defer password.Reset()
|
||||||
|
|
||||||
|
return argon2.IDKey(password.Bytes(), salt, 1, 64*1024, uint8(runtime.GOMAXPROCS(runtime.NumCPU())), chacha20poly1305.KeySize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read reads an unencrypted configuration file.
|
||||||
|
func (d *ding) read() error {
|
||||||
|
path := d.configPath()
|
||||||
|
if path == "" {
|
||||||
|
return errors.New("failed to retrieve configuration file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
d.configFile, err = os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer d.configFile.Close()
|
||||||
|
|
||||||
|
_, err = toml.NewDecoder(d.configFile).Decode(&d.config)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal deciphers and decodes the configuration file.
|
||||||
|
func (d *ding) unmarshal() error {
|
||||||
|
path := d.configPath()
|
||||||
|
if path == "" {
|
||||||
|
return errors.New("failed to retrieve configuration file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
conf, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
path = d.saltPath()
|
||||||
|
if path == "" {
|
||||||
|
return errors.New("failed to retrieve salt path")
|
||||||
|
}
|
||||||
|
|
||||||
|
var salt []byte
|
||||||
|
|
||||||
|
salt, err = os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedSalt := make([]byte, hex.DecodedLen(len(salt)))
|
||||||
|
|
||||||
|
if _, err = hex.Decode(decodedSalt, salt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var aead cipher.AEAD
|
||||||
|
|
||||||
|
aead, err = chacha20poly1305.NewX(d.deriveKey(decodedSalt))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conf) < aead.NonceSize() {
|
||||||
|
return errors.New("ciphered data size is greater than the nonce size")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, cipheredData := conf[:aead.NonceSize()], conf[aead.NonceSize():]
|
||||||
|
|
||||||
|
var decipheredData []byte
|
||||||
|
|
||||||
|
decipheredData, err = aead.Open(nil, nonce, cipheredData, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = toml.NewDecoder(bytes.NewBuffer(decipheredData)).Decode(&d.config)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePortRange(ports []string) error {
|
||||||
|
for _, port := range ports {
|
||||||
|
p, err := strconv.Atoi(port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p < 1 || p > 65535 {
|
||||||
|
return fmt.Errorf("%d: invalid port", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
12
cmd/ding/main.go
Normal file
12
cmd/ding/main.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.illuad.fr/adrien/ding/cli"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := cli.Run(); err != nil {
|
||||||
|
log.Fatal().Msg(err.Error())
|
||||||
|
}
|
||||||
|
}
|
26
go.mod
Normal file
26
go.mod
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module gitea.illuad.fr/adrien/ding
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
|
github.com/BurntSushi/toml v1.3.2
|
||||||
|
github.com/rs/zerolog v1.29.1
|
||||||
|
github.com/urfave/cli/v2 v2.25.7
|
||||||
|
github.com/wagslane/go-password-validator v0.3.0
|
||||||
|
golang.org/x/crypto v0.10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
|
golang.org/x/sys v0.9.0 // indirect
|
||||||
|
golang.org/x/term v0.9.0 // indirect
|
||||||
|
golang.org/x/text v0.10.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
90
go.sum
Normal file
90
go.sum
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||||
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||||
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||||
|
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||||
|
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||||
|
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||||
|
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
|
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||||
|
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||||
|
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
||||||
|
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||||
|
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
Loading…
Reference in New Issue
Block a user