Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go: [ "1.17", "1.18" ]
go: [ "1.25", "1.26" ]
arch:
- "GOARCH=amd64"
- "GOARCH=arm GOARM=5"
- "GOARCH=arm GOARM=6"
- "GOARCH=arm GOARM=7"
- "GOARCH=arm64"
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go }}

Expand All @@ -40,7 +40,7 @@ jobs:
fi

- name: Test
run: go test -v ./...
run: CGO_ENABLED=0 go test -v ./...

coverage:
runs-on: ubuntu-latest
Expand Down
4 changes: 3 additions & 1 deletion cmd/cli/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ func Add(hostname string, privKey, pubKey bool, owner, description string, confi

// publicKey := MustPromptString("PublicKey (optional)", false)
if !confirm {
ConfirmOrAbort("\nDo you want to add the above configuration?")
if err := ConfirmOrAbort("\nDo you want to add the above configuration?"); err != nil {
return err
}
}

// newline (not on stdout) to separate config
Expand Down
69 changes: 9 additions & 60 deletions cmd/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package cli
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"os"
"strings"
"time"

"github.com/go-playground/validator"
"github.com/go-playground/validator/v10"
"github.com/naggie/dsnet/lib"
"github.com/spf13/viper"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
Expand All @@ -31,9 +30,9 @@ type PeerConfig struct {
//ExternalIP net.UDPAddr `validate:"required,udp4_addr"`
// TODO support routing additional networks (AllowedIPs)
Networks []lib.JSONIPNet `validate:"required"`
PublicKey lib.JSONKey `validate:"required,len=44"`
PublicKey lib.JSONKey `validate:"required"`
PrivateKey lib.JSONKey `json:"-"` // omitted from config!
PresharedKey lib.JSONKey `validate:"required,len=44"`
PresharedKey lib.JSONKey `validate:"required"`
}

type DsnetConfig struct {
Expand Down Expand Up @@ -61,7 +60,7 @@ type DsnetConfig struct {
// extra networks available, will be added to AllowedIPs
Networks []lib.JSONIPNet `validate:"required"`
// TODO Default subnets to route via VPN
PrivateKey lib.JSONKey `validate:"required,len=44"`
PrivateKey lib.JSONKey `validate:"required"`
PostUp string
PostDown string
Peers []PeerConfig `validate:"dive"`
Expand All @@ -74,7 +73,7 @@ type DsnetConfig struct {
// it in to a struct
func LoadConfigFile() (*DsnetConfig, error) {
configFile := viper.GetString("config_file")
raw, err := ioutil.ReadFile(configFile)
raw, err := os.ReadFile(configFile)

if os.IsNotExist(err) {
return nil, fmt.Errorf("%s does not exist. `dsnet init` may be required", configFile)
Expand Down Expand Up @@ -112,13 +111,12 @@ func LoadConfigFile() (*DsnetConfig, error) {
// Save writes the configuration to disk
func (conf *DsnetConfig) Save() error {
configFile := viper.GetString("config_file")
_json, _ := json.MarshalIndent(conf, "", " ")
_json = append(_json, '\n')
err := ioutil.WriteFile(configFile, _json, 0600)
_json, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
return fmt.Errorf("failed to marshal config: %w", err)
}
return nil
_json = append(_json, '\n')
return os.WriteFile(configFile, _json, 0600)
}

// AddPeer adds a provided peer to the Peers list in the conf
Expand Down Expand Up @@ -180,55 +178,6 @@ func (conf *DsnetConfig) RemovePeer(hostname string) error {
return nil
}

func (conf DsnetConfig) GetWgPeerConfigs() []wgtypes.PeerConfig {
wgPeers := make([]wgtypes.PeerConfig, 0, len(conf.Peers))

for _, peer := range conf.Peers {
// create a new PSK in memory to avoid passing the same value by
// pointer to each peer (d'oh)
presharedKey := peer.PresharedKey.Key

// AllowedIPs = private IP + defined networks
allowedIPs := make([]net.IPNet, 0, len(peer.Networks)+2)

if len(peer.IP) > 0 {
allowedIPs = append(
allowedIPs,
net.IPNet{
IP: peer.IP,
Mask: net.IPMask{255, 255, 255, 255},
},
)
}

if len(peer.IP6) > 0 {
allowedIPs = append(
allowedIPs,
net.IPNet{
IP: peer.IP6,
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
},
)
}

for _, net := range peer.Networks {
allowedIPs = append(allowedIPs, net.IPNet)
}

wgPeers = append(wgPeers, wgtypes.PeerConfig{
PublicKey: peer.PublicKey.Key,
Remove: false,
UpdateOnly: false,
PresharedKey: &presharedKey,
Endpoint: nil,
ReplaceAllowedIPs: true,
AllowedIPs: allowedIPs,
})
}

return wgPeers
}

func (conf *DsnetConfig) Merge(patch map[string]interface{}) error {
// Merge the patch into the config

Expand Down
80 changes: 0 additions & 80 deletions cmd/cli/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,83 +278,3 @@ func TestRemovePeerLast(t *testing.T) {
t.Fatalf("expected 'laptop1', got '%s'", conf.Peers[0].Hostname)
}
}

func TestGetWgPeerConfigs(t *testing.T) {
conf := testDsnetConfig(t)
peer := testLibPeer(t, "laptop", "alice", net.IP{10, 0, 0, 2})
conf.AddPeer(peer)

wgPeers := conf.GetWgPeerConfigs()
if len(wgPeers) != 1 {
t.Fatalf("expected 1 wg peer, got %d", len(wgPeers))
}

p := wgPeers[0]
if p.PublicKey != peer.PublicKey.Key {
t.Fatal("public key mismatch")
}
if p.Remove {
t.Fatal("Remove should be false")
}
if !p.ReplaceAllowedIPs {
t.Fatal("ReplaceAllowedIPs should be true")
}
}

func TestGetWgPeerConfigsAllowedIPs(t *testing.T) {
conf := testDsnetConfig(t)
peer := testLibPeer(t, "laptop", "alice", net.IP{10, 0, 0, 2})
conf.AddPeer(peer)

wgPeers := conf.GetWgPeerConfigs()
p := wgPeers[0]

// Should have /32 for IPv4 + /128 for IPv6 = 2
if len(p.AllowedIPs) != 2 {
t.Fatalf("expected 2 AllowedIPs, got %d", len(p.AllowedIPs))
}

// First should be the /32
if p.AllowedIPs[0].IP.String() != "10.0.0.2" {
t.Fatalf("expected 10.0.0.2, got %s", p.AllowedIPs[0].IP)
}
ones, _ := p.AllowedIPs[0].Mask.Size()
if ones != 32 {
t.Fatalf("expected /32, got /%d", ones)
}
}

func TestGetWgPeerConfigsWithNetworks(t *testing.T) {
conf := testDsnetConfig(t)
peer := testLibPeer(t, "laptop", "alice", net.IP{10, 0, 0, 2})
peer.IP6 = nil // IPv4 only for simpler test

_, subnet, _ := net.ParseCIDR("192.168.1.0/24")
peer.Networks = []lib.JSONIPNet{{IPNet: *subnet}}

conf.AddPeer(peer)

wgPeers := conf.GetWgPeerConfigs()
p := wgPeers[0]

// /32 for IPv4 + extra network = 2
if len(p.AllowedIPs) != 2 {
t.Fatalf("expected 2 AllowedIPs, got %d", len(p.AllowedIPs))
}
}

func TestGetWgPeerConfigsPresharedKeyIsolation(t *testing.T) {
conf := testDsnetConfig(t)
peer1 := testLibPeer(t, "laptop1", "alice", net.IP{10, 0, 0, 2})
peer2 := testLibPeer(t, "laptop2", "bob", net.IP{10, 0, 0, 3})

conf.AddPeer(peer1)
conf.AddPeer(peer2)

wgPeers := conf.GetWgPeerConfigs()

// Each peer's preshared key pointer should point to different values
if *wgPeers[0].PresharedKey == *wgPeers[1].PresharedKey {
t.Fatal("preshared keys should be different between peers")
}
}
46 changes: 30 additions & 16 deletions cmd/cli/init.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package cli

import (
"crypto/rand"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"io"
"net"
"net/http"
"os"
Expand All @@ -23,8 +23,10 @@ func Init() error {

_, err := os.Stat(configFile)

if !os.IsNotExist(err) {
return fmt.Errorf("Refusing to overwrite existing %s", configFile)
if err == nil {
return fmt.Errorf("refusing to overwrite existing %s", configFile)
} else if !os.IsNotExist(err) {
return fmt.Errorf("could not stat %s: %w", configFile, err)
}

privateKey, err := lib.GenerateJSONPrivateKey()
Expand All @@ -42,11 +44,21 @@ func Init() error {
return err
}

network, err := getPrivateNet()
if err != nil {
return fmt.Errorf("%w - failed to generate private network", err)
}

network6, err := getULANet()
if err != nil {
return fmt.Errorf("%w - failed to generate ULA network", err)
}

conf := &DsnetConfig{
PrivateKey: privateKey,
ListenPort: listenPort,
Network: getPrivateNet(),
Network6: getULANet(),
Network: network,
Network6: network6,
Peers: []PeerConfig{},
Domain: "dsnet",
ExternalIP: externalIPV4,
Expand Down Expand Up @@ -85,31 +97,33 @@ func Init() error {
}

// get a random IPv4 /22 subnet on 10.0.0.0 (1023 hosts) (or /24?)
func getPrivateNet() lib.JSONIPNet {
func getPrivateNet() (lib.JSONIPNet, error) {
rbs := make([]byte, 2)
rand.Seed(time.Now().UTC().UnixNano())
rand.Read(rbs)
if _, err := rand.Read(rbs); err != nil {
return lib.JSONIPNet{}, fmt.Errorf("%w - failed to read random bytes", err)
}

return lib.JSONIPNet{
IPNet: net.IPNet{
IP: net.IP{10, rbs[0], rbs[1] << 2, 0},
Mask: net.IPMask{255, 255, 252, 0},
},
}
}, nil
}

func getULANet() lib.JSONIPNet {
func getULANet() (lib.JSONIPNet, error) {
rbs := make([]byte, 5)
rand.Seed(time.Now().UTC().UnixNano())
rand.Read(rbs)
if _, err := rand.Read(rbs); err != nil {
return lib.JSONIPNet{}, fmt.Errorf("%w - failed to read random bytes", err)
}

// fd00 prefix with 40 bit global id and zero (16 bit) subnet ID
return lib.JSONIPNet{
IPNet: net.IPNet{
IP: net.IP{0xfd, 0, rbs[0], rbs[1], rbs[2], rbs[3], rbs[4], 0, 0, 0, 0, 0, 0, 0, 0, 0},
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0},
},
}
}, nil
}

// TODO factor getExternalIP + getExternalIP6
Expand Down Expand Up @@ -143,7 +157,7 @@ func getExternalIP() (net.IP, error) {
defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -177,7 +191,7 @@ func getExternalIP6() (net.IP, error) {
defer resp.Body.Close()

if resp.StatusCode == http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading