Merge pull request #22 from naggie/ipv6

IPv6 support
This commit is contained in:
Callan Bryant 2020-10-29 17:00:47 +00:00 committed by GitHub
commit 227ed206a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 281 additions and 60 deletions

View File

@ -61,10 +61,12 @@ Main configuration example:
{
"ExternalIP": "198.51.100.2",
"ExternalIP6": "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
"ListenPort": 51820,
"Domain": "dsnet",
"InterfaceName": "dsnet",
"Network": "10.164.236.0/22",
"Network6": "fd00:7b31:106a:ae00::/64",
"IP": "10.164.236.1",
"DNS": "",
"Networks": [],
@ -76,6 +78,7 @@ Main configuration example:
"Owner": "naggie",
"Description": "Home server",
"IP": "10.164.236.2",
"IP6": "fd00:7b31:106a:ae00:44c3:29c3:53b1:a6f9",
"Added": "2020-05-07T10:04:46.336286992+01:00",
"Networks": [],
"PublicKey": "altJeQ/V52JZQrGcA9RiKcpZusYU6zMUJhl7Wbd9rX0=",
@ -88,11 +91,11 @@ Explanation of each field:
{
"ExternalIP": "198.51.100.2",
"ExternalIP6": "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
This is the external IP that will be the value of Endpoint for the server peer
in client configs. It is automatically detected by opening a socket or using an
external IP discovery service -- the first to give a valid public IPv4 will
win.
external IP discovery service -- the first to give a valid public IP will win.
"ListenPort": 51820,
@ -110,6 +113,7 @@ connection by polling the report file.
The wireguard interface name.
"Network": "10.164.236.0/22",
"Network6": "fd00:7b31:106a:ae00::/64",
The CIDR network to use when allocating IPs to peers. This subnet, a `/22` in
the `10.0.0.0/16` block is generated randomly to (probably) avoid collisions
@ -117,7 +121,10 @@ with other networks. There are 1022 addresses available. Addresses are
allocated to peers when peers are added with `dsnet add` using the lowest
available address.
A random ULA network with a subnet of 0 is generated for IPv6.
"IP": "10.164.236.1",
"IP6": "fd00:7b31:106a:ae00:44c3:29c3:53b1:a6f9",
This is the private VPN IP of the server peer. It is the first address in the
above pool.
@ -261,11 +268,54 @@ See
[etc/README.md](https://github.com/naggie/dsnet/blob/master/contrib/report_rendering/README.md)
for hugo and PHP code for rendering a similar table.
# Generating other config files
dsnet currently supports the generation of `wg-quick` configuration by default.
It can also generate VyOS/Vyatta configuration for EdgeOS/Unifi devices such as
the Edgerouter 4 using the
[wireguard-vyatta](https://github.com/WireGuard/wireguard-vyatta-ubnt) package.
To change the config file format, set the following environment variables:
* `DSNET_OUTPUT=vyatta`
* `DSNET_OUTPUT=wg-quick`
Example vyatta output:
configure
set interfaces wireguard wg0 address 10.165.52.3/22
set interfaces wireguard wg0 address fd00:7b31:106a:ae00:f7bb:bf31:201f:60ab/64
set interfaces wireguard wg0 route-allowed-ips true
set interfaces wireguard wg0 private-key cAtj1tbjGGmVoxdY78q9Sv0EgNlawbzffGWjajQkLFw=
set interfaces wireguard wg0 description dsnet
set interfaces wireguard wg0 peer PjxQM7OwVYvOJfORA1EluLw8CchSu7jLq92YYJi5ohY= endpoint 123.123.123.123:51820
set interfaces wireguard wg0 peer PjxQM7OwVYvOJfORA1EluLw8CchSu7jLq92YYJi5ohY= persistent-keepalive 25
set interfaces wireguard wg0 peer PjxQM7OwVYvOJfORA1EluLw8CchSu7jLq92YYJi5ohY= preshared-key w1FtOKoMEdnhsjREtSvpg1CHEKFzFzJWaQYZwaUCV38=
set interfaces wireguard wg0 peer PjxQM7OwVYvOJfORA1EluLw8CchSu7jLq92YYJi5ohY= allowed-ips 10.165.52.0/22
set interfaces wireguard wg0 peer PjxQM7OwVYvOJfORA1EluLw8CchSu7jLq92YYJi5ohY= allowed-ips fd00:7b31:106a:ae00::/64
commit; save
Replace `wg0` with an unused interface name in the range `wg0-wg999`.
# FAQ
> Does dsnet support IPv6?
Not currently but this is a [planned feature](https://github.com/naggie/dsnet/issues/1).
Yes! By default since version 0.2, a random ULA subnet is generated with a 0
subnet ID. Peers are allocated random addresses when added. Existing IPv4
configs will not be updated -- add a `Network6` subnet to the existing config
to allocate addresses to new peers.
Like IPv4, it's up to you if you want to provide NAT IPv6 access to the
internet; alternatively (and preferably) you can allocate a a real IPv6 subnet
such that all peers have a real globally routeable IPv6 address.
Upon initialisation, the server IPv4 and IPv6 external IP addresses are
discovered on a best-effort basis. Clients will have configuration configured
for the server IPv4 preferentially. If not IPv4 is configured, IPv6 is used;
this is to give the best chance of the VPN working regardless of the dodgy
network you're on.
> Is dsnet production ready?

76
add.go
View File

@ -3,40 +3,73 @@ package dsnet
import (
"fmt"
"os"
"strings"
"text/template"
"time"
)
const wgQuickPeerConf = `[Interface]
Address = {{ .Peer.IP }}/22
{{ if gt (.DsnetConfig.Network.IPNet.IP | len) 0 -}}
Address={{ .Peer.IP }}/{{ .CidrSize }}
{{ end -}}
{{ if gt (.DsnetConfig.Network6.IPNet.IP | len) 0 -}}
Address={{ .Peer.IP6 }}/{{ .CidrSize6 }}
{{ end -}}
PrivateKey={{ .Peer.PrivateKey.Key }}
{{- if .DsnetConfig.DNS }}
DNS = {{ .DsnetConfig.DNS }}
DNS={{ .DsnetConfig.DNS }}
{{ end }}
[Peer]
PublicKey={{ .DsnetConfig.PrivateKey.PublicKey.Key }}
PresharedKey={{ .Peer.PresharedKey.Key }}
{{ if gt (.DsnetConfig.ExternalIP | len) 0 -}}
Endpoint={{ .DsnetConfig.ExternalIP }}:{{ .DsnetConfig.ListenPort }}
AllowedIPs={{ .AllowedIPs }}
{{ else -}}
Endpoint={{ .DsnetConfig.ExternalIP6 }}:{{ .DsnetConfig.ListenPort }}
{{ end -}}
PersistentKeepalive={{ .Keepalive }}
{{ if gt (.DsnetConfig.Network.IPNet.IP | len) 0 -}}
AllowedIPs={{ .DsnetConfig.Network }}
{{ end -}}
{{ if gt (.DsnetConfig.Network6.IPNet.IP | len) 0 -}}
AllowedIPs={{ .DsnetConfig.Network6 }}
{{ end -}}
{{ range .DsnetConfig.Networks -}}
AllowedIPs={{ . }}
{{ end -}}
`
// TODO use random wg0-wg999 to hopefully avoid conflict by default?
const vyattaPeerConf = `configure
set interfaces wireguard wg0 address {{ .Peer.IP }}/{{ .Cidrmask }}
{{ if gt (.DsnetConfig.Network.IPNet.IP | len) 0 -}}
set interfaces wireguard wg0 address {{ .Peer.IP }}/{{ .CidrSize }}
{{ end -}}
{{ if gt (.DsnetConfig.Network6.IPNet.IP | len) 0 -}}
set interfaces wireguard wg0 address {{ .Peer.IP6 }}/{{ .CidrSize6 }}
{{ end -}}
set interfaces wireguard wg0 route-allowed-ips true
set interfaces wireguard wg0 private-key {{ .Peer.PrivateKey.Key }}
set interfaces wireguard wg0 description {{ conf.InterfaceName }}
set interfaces wireguard wg0 description {{ .DsnetConfig.InterfaceName }}
{{- if .DsnetConfig.DNS }}
#set service dns forwarding name-server {{ .DsnetConfig.DNS }}
{{ end }}
{{ if gt (.DsnetConfig.ExternalIP | len) 0 -}}
set interfaces wireguard wg0 peer {{ .DsnetConfig.PrivateKey.PublicKey.Key }} endpoint {{ .DsnetConfig.ExternalIP }}:{{ .DsnetConfig.ListenPort }}
set interfaces wireguard wg0 peer {{ .DsnetConfig.PrivateKey.PublicKey.Key }} allowed-ips {{ .AllowedIPs }}
{{ else -}}
set interfaces wireguard wg0 peer {{ .DsnetConfig.PrivateKey.PublicKey.Key }} endpoint {{ .DsnetConfig.ExternalIP6 }}:{{ .DsnetConfig.ListenPort }}
{{ end -}}
set interfaces wireguard wg0 peer {{ .DsnetConfig.PrivateKey.PublicKey.Key }} persistent-keepalive {{ .Keepalive }}
set interfaces wireguard wg0 peer {{ .DsnetConfig.PrivateKey.PublicKey.Key }} preshared-key {{ .Peer.PresharedKey.Key }}
{{ if gt (.DsnetConfig.Network.IPNet.IP | len) 0 -}}
set interfaces wireguard wg0 peer {{ .DsnetConfig.PrivateKey.PublicKey.Key }} allowed-ips {{ .DsnetConfig.Network }}
{{ end -}}
{{ if gt (.DsnetConfig.Network6.IPNet.IP | len) 0 -}}
set interfaces wireguard wg0 peer {{ .DsnetConfig.PrivateKey.PublicKey.Key }} allowed-ips {{ .DsnetConfig.Network6 }}
{{ end -}}
{{ range .DsnetConfig.Networks -}}
set interfaces wireguard wg0 peer {{ .DsnetConfig.PrivateKey.PublicKey.Key }} allowed-ips {{ . }}
{{ end -}}
commit; save
`
@ -62,8 +95,6 @@ func Add() {
privateKey := GenerateJSONPrivateKey()
publicKey := privateKey.PublicKey()
IP := conf.MustAllocateIP()
peer := PeerConfig{
Owner: owner,
Hostname: hostname,
@ -72,10 +103,21 @@ func Add() {
PublicKey: publicKey,
PrivateKey: privateKey, // omitted from server config JSON!
PresharedKey: GenerateJSONKey(),
IP: IP,
Networks: []JSONIPNet{},
}
if len(conf.Network.IPNet.Mask) > 0 {
peer.IP = conf.MustAllocateIP()
}
if len(conf.Network6.IPNet.Mask) > 0 {
peer.IP6 = conf.MustAllocateIP6()
}
if len(conf.IP) == 0 && len(conf.IP6) == 0 {
ExitFail("No IPv4 or IPv6 network defined in config")
}
conf.MustAddPeer(peer)
PrintPeerCfg(peer, conf)
conf.MustSave()
@ -83,13 +125,6 @@ func Add() {
}
func PrintPeerCfg(peer PeerConfig, conf *DsnetConfig) {
allowedIPsStr := make([]string, len(conf.Networks)+1)
allowedIPsStr[0] = conf.Network.String()
for i, net := range conf.Networks {
allowedIPsStr[i+1] = net.String()
}
var peerConf string
switch os.Getenv("DSNET_OUTPUT") {
@ -103,15 +138,16 @@ func PrintPeerCfg(peer PeerConfig, conf *DsnetConfig) {
ExitFail("Unrecognised DSNET_OUTPUT type")
}
cidrmask, _ := conf.Network.IPNet.Mask.Size()
cidrSize, _ := conf.Network.IPNet.Mask.Size()
cidrSize6, _ := conf.Network6.IPNet.Mask.Size()
t := template.Must(template.New("peerConf").Parse(peerConf))
err := t.Execute(os.Stdout, map[string]interface{}{
"Peer": peer,
"DsnetConfig": conf,
"Keepalive": time.Duration(KEEPALIVE).Seconds(),
"AllowedIPs": strings.Join(allowedIPsStr, ","),
"Cidrmask": cidrmask,
"CidrSize": cidrSize,
"CidrSize6": cidrSize6,
})
check(err)
}

View File

@ -3,6 +3,7 @@ package dsnet
import (
"encoding/json"
"io/ioutil"
"math/rand"
"net"
"os"
"time"
@ -20,7 +21,8 @@ type PeerConfig struct {
// Description of what the host is and/or does
Description string `validate:"required,gte=1,lte=255"`
// Internal VPN IP address. Added to AllowedIPs in server config as a /32
IP net.IP `validate:"required`
IP net.IP
IP6 net.IP
Added time.Time `validate:"required"`
// TODO ExternalIP support (Endpoint)
//ExternalIP net.UDPAddr `validate:"required,udp4_addr"`
@ -34,15 +36,18 @@ type PeerConfig struct {
type DsnetConfig struct {
// domain to append to hostnames. Relies on separate DNS server for
// resolution. Informational only.
ExternalIP net.IP `validate:"required"`
ExternalIP net.IP
ExternalIP6 net.IP
ListenPort int `validate:"gte=1024,lte=65535"`
Domain string `validate:"required,gte=1,lte=255"`
InterfaceName string `validate:"required,gte=1,lte=255"`
// IP network from which to allocate automatic sequential addresses
// Network is chosen randomly when not specified
Network JSONIPNet `validate:"required"`
IP net.IP `validate:"required"`
DNS net.IP
Network JSONIPNet `validate:"required"`
Network6 JSONIPNet `validate:"required"`
IP net.IP
IP6 net.IP
DNS net.IP
// extra networks available, will be added to AllowedIPs
Networks []JSONIPNet `validate:"required"`
// TODO Default subnets to route via VPN
@ -69,6 +74,10 @@ func MustLoadDsnetConfig() *DsnetConfig {
err = validator.New().Struct(conf)
check(err)
if len(conf.ExternalIP) == 0 && len(conf.ExternalIP6) == 0 {
ExitFail("Config does not contain ExternalIP or ExternalIP6")
}
return &conf
}
@ -127,16 +136,16 @@ func (conf *DsnetConfig) MustRemovePeer(hostname string) {
// remove peer from slice, retaining order
copy(conf.Peers[peerIndex:], conf.Peers[peerIndex+1:]) // shift left
conf.Peers = conf.Peers[:len(conf.Peers)-1] // truncate
conf.Peers = conf.Peers[:len(conf.Peers)-1] // truncate
}
func (conf DsnetConfig) IPAllocated(IP net.IP) bool {
if IP.Equal(conf.IP) {
if IP.Equal(conf.IP) || IP.Equal(conf.IP6) {
return true
}
for _, peer := range conf.Peers {
if IP.Equal(peer.IP) {
if IP.Equal(peer.IP) || IP.Equal(peer.IP6) {
return true
}
@ -150,16 +159,21 @@ func (conf DsnetConfig) IPAllocated(IP net.IP) bool {
return false
}
// choose a free IP for a new Peer
// choose a free IPv4 for a new Peer (sequential allocation)
func (conf DsnetConfig) MustAllocateIP() net.IP {
network := conf.Network.IPNet
ones, bits := network.Mask.Size()
zeros := bits - ones
min := 1 // avoids network addr
max := (1 << zeros) - 2 // avoids broadcast addr + overflow
// avoids network addr
min := 1
// avoids broadcast addr + overflow
max := (1 << zeros) - 2
IP := make(net.IP, len(network.IP))
for i := min; i <= max; i++ {
IP := make(net.IP, len(network.IP))
// dst, src!
copy(IP, network.IP)
// OR the host part with the network part
@ -178,6 +192,37 @@ func (conf DsnetConfig) MustAllocateIP() net.IP {
return net.IP{}
}
// choose a free IPv6 for a new Peer (pseudorandom allocation)
func (conf DsnetConfig) MustAllocateIP6() net.IP {
network := conf.Network6.IPNet
ones, bits := network.Mask.Size()
zeros := bits - ones
rbs := make([]byte, zeros)
rand.Seed(time.Now().UTC().UnixNano())
IP := make(net.IP, len(network.IP))
for i := 0; i <= 10000; i++ {
rand.Read(rbs)
// dst, src! Copy prefix of IP
copy(IP, network.IP)
// OR the host part with the network part
for j := ones / 8; j < len(IP); j++ {
IP[j] = IP[j] | rbs[j]
}
if !conf.IPAllocated(IP) {
return IP
}
}
ExitFail("Could not allocate random IPv6 after 10000 tries. This was highly unlikely!")
return net.IP{}
}
func (conf DsnetConfig) GetWgPeerConfigs() []wgtypes.PeerConfig {
wgPeers := make([]wgtypes.PeerConfig, 0, len(conf.Peers))
@ -187,10 +232,26 @@ func (conf DsnetConfig) GetWgPeerConfigs() []wgtypes.PeerConfig {
presharedKey := peer.PresharedKey.Key
// AllowedIPs = private IP + defined networks
allowedIPs := make([]net.IPNet, len(peer.Networks)+1)
allowedIPs[0] = net.IPNet{
IP: peer.IP,
Mask: net.IPMask{255, 255, 255, 255},
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 i, net := range peer.Networks {

View File

@ -27,7 +27,7 @@ const (
var (
// populated with LDFLAGS, see do-release.sh
VERSION = "unknown"
VERSION = "unknown"
GIT_COMMIT = "unknown"
BUILD_DATE = "unknown"
)

View File

@ -12,14 +12,30 @@ type JSONIPNet struct {
}
func (n JSONIPNet) MarshalJSON() ([]byte, error) {
return []byte("\"" + n.IPNet.String() + "\""), nil
if len(n.IPNet.IP) == 0 {
return []byte("\"\""), nil
} else {
return []byte("\"" + n.IPNet.String() + "\""), nil
}
}
func (n *JSONIPNet) UnmarshalJSON(b []byte) error {
cidr := strings.Trim(string(b), "\"")
if cidr == "" {
// Leave as empty/uninitialised IPNet. A bit like omitempty behaviour,
// but we can leave the field there and blank which is useful if the
// user wishes to add the cidr manually.
return nil
}
IP, IPNet, err := net.ParseCIDR(cidr)
IPNet.IP = IP
n.IPNet = *IPNet
if err == nil {
IPNet.IP = IP
n.IPNet = *IPNet
}
return err
}

90
init.go
View File

@ -21,27 +21,34 @@ func Init() {
conf := DsnetConfig{
PrivateKey: GenerateJSONPrivateKey(),
ListenPort: DEFAULT_LISTEN_PORT,
Network: getRandomNetwork(),
Network: getPrivateNet(),
Network6: getULANet(),
Peers: []PeerConfig{},
Domain: "dsnet",
ReportFile: DEFAULT_REPORT_FILE,
ExternalIP: getExternalIP(),
ExternalIP6: getExternalIP6(),
InterfaceName: DEFAULT_INTERFACE_NAME,
Networks: []JSONIPNet{},
}
IP := conf.MustAllocateIP()
conf.IP = IP
conf.IP = conf.MustAllocateIP()
conf.IP6 = conf.MustAllocateIP6()
if len(conf.ExternalIP) == 0 && len(conf.ExternalIP6) == 0 {
ExitFail("Could not determine any external IP, v4 or v6")
}
// DNS not set by default
//conf.DNS = IP
conf.MustSave()
fmt.Printf("Config written to %s. Please check/edit.", CONFIG_FILE)
fmt.Printf("Config written to %s. Please check/edit.\n", CONFIG_FILE)
}
// get a random /22 subnet on 10.0.0.0 (1023 hosts) (or /24?)
func getRandomNetwork() JSONIPNet {
// get a random IPv4 /22 subnet on 10.0.0.0 (1023 hosts) (or /24?)
func getPrivateNet() JSONIPNet {
rbs := make([]byte, 2)
rand.Seed(time.Now().UTC().UnixNano())
rand.Read(rbs)
@ -54,19 +61,37 @@ func getRandomNetwork() JSONIPNet {
}
}
// TODO support IPv6
func getULANet() JSONIPNet {
rbs := make([]byte, 5)
rand.Seed(time.Now().UTC().UnixNano())
rand.Read(rbs)
// fd00 prefix with 40 bit global id and zero (16 bit) subnet ID
return JSONIPNet{
IPNet: net.IPNet{
net.IP{0xfd, 0, rbs[0], rbs[1], rbs[2], rbs[3], rbs[4], 0, 0, 0, 0, 0, 0, 0, 0, 0},
net.IPMask{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0, 0},
},
}
}
// TODO factor getExternalIP + getExternalIP6
func getExternalIP() net.IP {
conn, err := net.Dial("udp", "8.8.8.8:80")
check(err, "Could not detect internet connection")
defer conn.Close()
var IP net.IP
// arbitrary external IP is used (one that's guaranteed to route outside.
// In this case, Google's DNS server. Doesn't actually need to be online.)
conn, err := net.Dial("udp", "8.8.8.8:53")
if err == nil {
defer conn.Close()
localAddr := conn.LocalAddr().String()
IP := net.ParseIP(strings.Split(localAddr, ":")[0])
IP = IP.To4()
localAddr := conn.LocalAddr().String()
IP = net.ParseIP(strings.Split(localAddr, ":")[0])
IP = IP.To4()
if !(IP[0] == 10 || (IP[0] == 172 && IP[1] >= 16 && IP[1] <= 31) || (IP[0] == 192 && IP[1] == 168)) {
// not private, so public
return IP
if !(IP[0] == 10 || (IP[0] == 172 && IP[1] >= 16 && IP[1] <= 31) || (IP[0] == 192 && IP[1] == 168)) {
// not private, so public
return IP
}
}
// detect private IP and use icanhazip.com instead
@ -86,3 +111,36 @@ func getExternalIP() net.IP {
return net.IP{}
}
func getExternalIP6() net.IP {
var IP net.IP
conn, err := net.Dial("udp", "2001:4860:4860::8888:53")
if err == nil {
defer conn.Close()
localAddr := conn.LocalAddr().String()
IP = net.ParseIP(strings.Split(localAddr, ":")[0])
// check is not a ULA
if IP[0] != 0xfd && IP[0] != 0xfc {
return IP
}
}
client := http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get("https://ipv6.icanhazip.com/")
if err == nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
check(err)
IP = net.ParseIP(strings.TrimSpace(string(body)))
return IP
}
}
return net.IP{}
}

View File

@ -9,7 +9,7 @@ import (
func check(e error, optMsg ...string) {
if e != nil {
if (len(optMsg) > 0) {
if len(optMsg) > 0 {
ExitFail("%s - %s", e, strings.Join(optMsg, " "))
}
ExitFail("%s", e)