Compare commits

...

7 Commits

Author SHA1 Message Date
Neil Alexander
bec40ed9fb Add tunnel helper to ipv6rwc 2024-11-14 23:12:05 +00:00
Klemens Nanni
75d2080e53 Set groups when dropping privileges to not leak supplementary group access (#1202)
Changing the real and effective user/group IDs and the saved
set-user/group-ID is not enough to get rid of intial access permissions.

The list of groups must be cleared also, otherwise a process changing
from, e.g. `root:root` to `nobody:nobody` retains rights to access
`:wheel` files (assuming `root` is a member of the `wheel` group).

For example:
```
# id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)
# ./yggdrasil -autoconf -logto /dev/null -user nobody &
[1] 4337
# ps -o command,user,group,supgrp -U nobody
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   wheel,kmem,sys,tty,operator,staff,guest
```

Fix that so the process runs as mere
```
COMMAND          USER     GROUP    SUPGRP
./yggdrasil -aut nobody   nobody   nobody
```

Fixes #927.
2024-11-11 19:28:28 +00:00
Klemens Nanni
834680045a Create admin socket synchronously before privdrop (#1201)
Creating UNIX sockets the listen() goroutine that races against the main
one dropping to an unprivileged user may cause startup failure when
privdrop happens before privileged filesystem access.

Setup or fail in New() and only do listen(2) in listen() to avoid this.

```
# yggdrasil -autoconf -user nobody
2024/11/03 21:15:27 Build name: yggdrasil-go
2024/11/03 21:15:27 Build version: 0.5.9
...
2024/11/03 21:15:27 Admin socket failed to listen: listen unix /var/run/yggdrasil.sock: bind: permission denied
```

Rerun, now the order is flipped:
```
# yggdrasil -autoconf -user nobody
2024/11/03 21:15:34 Build name: yggdrasil-go
2024/11/03 21:15:34 Build version: 0.5.9
[...]
2024/11/03 21:15:34 UNIX admin socket listening on /var/run/yggdrasil.sock
[...]
```

Fixes #927.
2024-11-11 19:27:02 +00:00
Neil Alexander
eef613993f Raise link error when SNI supplied on unsupported link type
Some checks failed
Yggdrasil / Lint (push) Has been cancelled
Yggdrasil / Analyse (push) Has been cancelled
Yggdrasil / Build & Test (Linux, Go ${{ matrix.goversion }}) (1.21) (push) Has been cancelled
Yggdrasil / Build & Test (Linux, Go ${{ matrix.goversion }}) (1.22) (push) Has been cancelled
Yggdrasil / Build & Test (Linux, Go ${{ matrix.goversion }}) (1.23) (push) Has been cancelled
Yggdrasil / Build & Test (Windows, Go ${{ matrix.goversion }}) (1.21) (push) Has been cancelled
Yggdrasil / Build & Test (Windows, Go ${{ matrix.goversion }}) (1.22) (push) Has been cancelled
Yggdrasil / Build & Test (Windows, Go ${{ matrix.goversion }}) (1.23) (push) Has been cancelled
Yggdrasil / Build & Test (macOS, Go ${{ matrix.goversion }}) (1.21) (push) Has been cancelled
Yggdrasil / Build & Test (macOS, Go ${{ matrix.goversion }}) (1.22) (push) Has been cancelled
Yggdrasil / Build & Test (macOS, Go ${{ matrix.goversion }}) (1.23) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (freebsd, 1.21) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (freebsd, 1.22) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (freebsd, 1.23) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (openbsd, 1.21) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (openbsd, 1.22) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (openbsd, 1.23) (push) Has been cancelled
Yggdrasil / All tests passed (push) Has been cancelled
Closes #1196
2024-10-27 21:06:56 +00:00
Neil Alexander
ff0ef7ff56 Update comments in default configuration file 2024-10-27 20:59:05 +00:00
Neil Alexander
ef110b0181 Update Debian package metadata 2024-10-27 20:38:15 +00:00
Neil Alexander
b20ad846a1 When IfName is none, start queue goroutine, otherwise iprwc blocks and some handlers don't run
Some checks failed
Yggdrasil / Lint (push) Has been cancelled
Yggdrasil / Analyse (push) Has been cancelled
Yggdrasil / Build & Test (Linux, Go ${{ matrix.goversion }}) (1.21) (push) Has been cancelled
Yggdrasil / Build & Test (Linux, Go ${{ matrix.goversion }}) (1.22) (push) Has been cancelled
Yggdrasil / Build & Test (Linux, Go ${{ matrix.goversion }}) (1.23) (push) Has been cancelled
Yggdrasil / Build & Test (Windows, Go ${{ matrix.goversion }}) (1.21) (push) Has been cancelled
Yggdrasil / Build & Test (Windows, Go ${{ matrix.goversion }}) (1.22) (push) Has been cancelled
Yggdrasil / Build & Test (Windows, Go ${{ matrix.goversion }}) (1.23) (push) Has been cancelled
Yggdrasil / Build & Test (macOS, Go ${{ matrix.goversion }}) (1.21) (push) Has been cancelled
Yggdrasil / Build & Test (macOS, Go ${{ matrix.goversion }}) (1.22) (push) Has been cancelled
Yggdrasil / Build & Test (macOS, Go ${{ matrix.goversion }}) (1.23) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (freebsd, 1.21) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (freebsd, 1.22) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (freebsd, 1.23) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (openbsd, 1.21) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (openbsd, 1.22) (push) Has been cancelled
Yggdrasil / Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }}) (openbsd, 1.23) (push) Has been cancelled
Yggdrasil / All tests passed (push) Has been cancelled
2024-10-20 21:28:04 +01:00
12 changed files with 169 additions and 143 deletions

View File

@@ -53,6 +53,9 @@ func chuser(user string) error {
gid, _ := strconv.ParseUint(g.Gid, 10, 32) gid, _ := strconv.ParseUint(g.Gid, 10, 32)
var err error var err error
if gid < math.MaxInt { if gid < math.MaxInt {
if err := syscall.Setgroups([]int{int(gid)}); err != nil {
return fmt.Errorf("failed to setgroups %d: %v", gid, err)
}
err = syscall.Setgid(int(gid)) err = syscall.Setgid(int(gid))
} else { } else {
err = errors.New("gid too big") err = errors.New("gid too big")
@@ -63,6 +66,9 @@ func chuser(user string) error {
} }
} else if u != nil { } else if u != nil {
gid, _ := strconv.ParseUint(u.Gid, 10, 32) gid, _ := strconv.ParseUint(u.Gid, 10, 32)
if err := syscall.Setgroups([]int{int(uint32(gid))}); err != nil {
return fmt.Errorf("failed to setgroups %d: %v", gid, err)
}
err := syscall.Setgid(int(uint32(gid))) err := syscall.Setgid(int(uint32(gid)))
if err != nil { if err != nil {
return fmt.Errorf("failed to setgid %d: %v", gid, err) return fmt.Errorf("failed to setgid %d: %v", gid, err)

View File

@@ -50,11 +50,12 @@ echo 9 > /tmp/$PKGNAME/debian/compat
cat > /tmp/$PKGNAME/debian/control << EOF cat > /tmp/$PKGNAME/debian/control << EOF
Package: $PKGNAME Package: $PKGNAME
Version: $PKGVERSION Version: $PKGVERSION
Section: contrib/net Section: golang
Priority: extra Priority: optional
Architecture: $PKGARCH Architecture: $PKGARCH
Replaces: $PKGREPLACES Replaces: $PKGREPLACES
Conflicts: $PKGREPLACES Conflicts: $PKGREPLACES
Depends: systemd
Maintainer: Neil Alexander <neilalexander@users.noreply.github.com> Maintainer: Neil Alexander <neilalexander@users.noreply.github.com>
Description: Yggdrasil Network Description: Yggdrasil Network
Yggdrasil is an early-stage implementation of a fully end-to-end encrypted IPv6 Yggdrasil is an early-stage implementation of a fully end-to-end encrypted IPv6
@@ -102,7 +103,7 @@ then
echo "Normalising and updating /etc/yggdrasil/yggdrasil.conf" echo "Normalising and updating /etc/yggdrasil/yggdrasil.conf"
/usr/bin/yggdrasil -useconf -normaliseconf < /var/backups/yggdrasil.conf.`date +%Y%m%d` > /etc/yggdrasil/yggdrasil.conf /usr/bin/yggdrasil -useconf -normaliseconf < /var/backups/yggdrasil.conf.`date +%Y%m%d` > /etc/yggdrasil/yggdrasil.conf
chown root:yggdrasil /etc/yggdrasil/yggdrasil.conf chown root:yggdrasil /etc/yggdrasil/yggdrasil.conf
chmod 640 /etc/yggdrasil/yggdrasil.conf chmod 640 /etc/yggdrasil/yggdrasil.conf
else else

View File

@@ -83,6 +83,52 @@ func New(c *core.Core, log core.Logger, opts ...SetupOption) (*AdminSocket, erro
if a.config.listenaddr == "none" || a.config.listenaddr == "" { if a.config.listenaddr == "none" || a.config.listenaddr == "" {
return nil, nil return nil, nil
} }
listenaddr := string(a.config.listenaddr)
u, err := url.Parse(listenaddr)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
if _, err := os.Stat(u.Path); err == nil {
a.log.Debugln("Admin socket", u.Path, "already exists, trying to clean up")
if _, err := net.DialTimeout("unix", u.Path, time.Second*2); err == nil || err.(net.Error).Timeout() {
a.log.Errorln("Admin socket", u.Path, "already exists and is in use by another process")
os.Exit(1)
} else {
if err := os.Remove(u.Path); err == nil {
a.log.Debugln(u.Path, "was cleaned up")
} else {
a.log.Errorln(u.Path, "already exists and was not cleaned up:", err)
os.Exit(1)
}
}
}
a.listener, err = net.Listen("unix", u.Path)
if err == nil {
switch u.Path[:1] {
case "@": // maybe abstract namespace
default:
if err := os.Chmod(u.Path, 0660); err != nil {
a.log.Warnln("WARNING:", u.Path, "may have unsafe permissions!")
}
}
}
case "tcp":
a.listener, err = net.Listen("tcp", u.Host)
default:
a.listener, err = net.Listen("tcp", listenaddr)
}
} else {
a.listener, err = net.Listen("tcp", listenaddr)
}
if err != nil {
a.log.Errorf("Admin socket failed to listen: %v", err)
os.Exit(1)
}
a.log.Infof("%s admin socket listening on %s",
strings.ToUpper(a.listener.Addr().Network()),
a.listener.Addr().String())
_ = a.AddHandler("list", "List available commands", []string{}, func(_ json.RawMessage) (interface{}, error) { _ = a.AddHandler("list", "List available commands", []string{}, func(_ json.RawMessage) (interface{}, error) {
res := &ListResponse{} res := &ListResponse{}
for name, handler := range a.handlers { for name, handler := range a.handlers {
@@ -233,50 +279,6 @@ func (a *AdminSocket) Stop() error {
// listen is run by start and manages API connections. // listen is run by start and manages API connections.
func (a *AdminSocket) listen() { func (a *AdminSocket) listen() {
listenaddr := string(a.config.listenaddr)
u, err := url.Parse(listenaddr)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
if _, err := os.Stat(u.Path); err == nil {
a.log.Debugln("Admin socket", u.Path, "already exists, trying to clean up")
if _, err := net.DialTimeout("unix", u.Path, time.Second*2); err == nil || err.(net.Error).Timeout() {
a.log.Errorln("Admin socket", u.Path, "already exists and is in use by another process")
os.Exit(1)
} else {
if err := os.Remove(u.Path); err == nil {
a.log.Debugln(u.Path, "was cleaned up")
} else {
a.log.Errorln(u.Path, "already exists and was not cleaned up:", err)
os.Exit(1)
}
}
}
a.listener, err = net.Listen("unix", u.Path)
if err == nil {
switch u.Path[:1] {
case "@": // maybe abstract namespace
default:
if err := os.Chmod(u.Path, 0660); err != nil {
a.log.Warnln("WARNING:", u.Path, "may have unsafe permissions!")
}
}
}
case "tcp":
a.listener, err = net.Listen("tcp", u.Host)
default:
a.listener, err = net.Listen("tcp", listenaddr)
}
} else {
a.listener, err = net.Listen("tcp", listenaddr)
}
if err != nil {
a.log.Errorf("Admin socket failed to listen: %v", err)
os.Exit(1)
}
a.log.Infof("%s admin socket listening on %s",
strings.ToUpper(a.listener.Addr().Network()),
a.listener.Addr().String())
defer a.listener.Close() defer a.listener.Close()
for { for {
conn, err := a.listener.Accept() conn, err := a.listener.Accept()

View File

@@ -43,17 +43,17 @@ type NodeConfig struct {
PrivateKey KeyBytes `json:",omitempty" comment:"Your private key. DO NOT share this with anyone!"` PrivateKey KeyBytes `json:",omitempty" comment:"Your private key. DO NOT share this with anyone!"`
PrivateKeyPath string `json:",omitempty" comment:"The path to your private key file in PEM format."` PrivateKeyPath string `json:",omitempty" comment:"The path to your private key file in PEM format."`
Certificate *tls.Certificate `json:"-"` Certificate *tls.Certificate `json:"-"`
Peers []string `comment:"List of connection strings for outbound peer connections in URI format,\ne.g. tls://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j. These connections\nwill obey the operating system routing table, therefore you should\nuse this section when you may connect via different interfaces."` Peers []string `comment:"List of outbound peer connection strings (e.g. tls://a.b.c.d:e or\nsocks://a.b.c.d:e/f.g.h.i:j). Connection strings can contain options,\nsee https://yggdrasil-network.github.io/configurationref.html#peers.\nYggdrasil has no concept of bootstrap nodes - all network traffic\nwill transit peer connections. Therefore make sure to only peer with\nnearby nodes that have good connectivity and low latency. Avoid adding\npeers to this list from distant countries as this will worsen your\nnode's connectivity and performance considerably."`
InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ \"tls://a.b.c.d:e\" ] }.\nNote that SOCKS peerings will NOT be affected by this option and should\ngo in the \"Peers\" section instead."` InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ \"tls://a.b.c.d:e\" ] }.\nYou should only use this option if your machine is multi-homed and you\nwant to establish outbound peer connections on different interfaces.\nOtherwise you should use \"Peers\"."`
Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."` Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nThis is not required if you wish to establish outbound peerings only.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."`
AdminListen string `json:",omitempty" comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."` AdminListen string `json:",omitempty" comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."`
MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Each entry in the list should be a json object which may\ncontain Regex, Beacon, Listen, and Port. Regex is a regular expression\nwhich is matched against an interface name, and interfaces use the\nfirst configuration that they match gainst. Beacon configures whether\nor not the node should send link-local multicast beacons to advertise\ntheir presence, while listening for incoming connections on Port.\nListen controls whether or not the node listens for multicast beacons\nand opens outgoing connections."` MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Each entry in the list should be a json object which may\ncontain Regex, Beacon, Listen, and Port. Regex is a regular expression\nwhich is matched against an interface name, and interfaces use the\nfirst configuration that they match gainst. Beacon configures whether\nor not the node should send link-local multicast beacons to advertise\ntheir presence, while listening for incoming connections on Port.\nListen controls whether or not the node listens for multicast beacons\nand opens outgoing connections."`
AllowedPublicKeys []string `comment:"List of peer public keys to allow incoming peering connections\nfrom. If left empty/undefined then all connections will be allowed\nby default. This does not affect outgoing peerings, nor does it\naffect link-local peers discovered via multicast."` AllowedPublicKeys []string `comment:"List of peer public keys to allow incoming peering connections\nfrom. If left empty/undefined then all connections will be allowed\nby default. This does not affect outgoing peerings, nor does it\naffect link-local peers discovered via multicast.\nWARNING: THIS IS NOT A FIREWALL and DOES NOT limit who can reach\nopen ports or services running on your machine!"`
IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."` IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."`
IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."` IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
LogLookups bool `json:",omitempty"` LogLookups bool `json:",omitempty"`
NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."` NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."`
NodeInfo map[string]interface{} `comment:"Optional node info. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."` NodeInfo map[string]interface{} `comment:"Optional nodeinfo. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."`
} }
type MulticastInterfaceConfig struct { type MulticastInterfaceConfig struct {

View File

@@ -126,6 +126,7 @@ const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid")
const ErrLinkPasswordInvalid = linkError("invalid password supplied") const ErrLinkPasswordInvalid = linkError("invalid password supplied")
const ErrLinkUnrecognisedSchema = linkError("link schema unknown") const ErrLinkUnrecognisedSchema = linkError("link schema unknown")
const ErrLinkMaxBackoffInvalid = linkError("max backoff duration invalid") const ErrLinkMaxBackoffInvalid = linkError("max backoff duration invalid")
const ErrLinkSNINotSupported = linkError("SNI not supported on this link type")
func (l *links) add(u *url.URL, sintf string, linkType linkType) error { func (l *links) add(u *url.URL, sintf string, linkType linkType) error {
var retErr error var retErr error

View File

@@ -23,6 +23,9 @@ func (l *links) newLinkSOCKS() *linkSOCKS {
} }
func (l *linkSOCKS) dial(_ context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { func (l *linkSOCKS) dial(_ context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
if url.Scheme != "sockstls" && options.tlsSNI != "" {
return nil, ErrLinkSNINotSupported
}
var proxyAuth *proxy.Auth var proxyAuth *proxy.Auth
if url.User != nil && url.User.Username() != "" { if url.User != nil && url.User.Username() != "" {
proxyAuth = &proxy.Auth{ proxyAuth = &proxy.Auth{

View File

@@ -67,6 +67,9 @@ func (l *linkTCP) dialersFor(url *url.URL, info linkInfo) ([]*tcpDialer, error)
} }
func (l *linkTCP) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { func (l *linkTCP) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
if options.tlsSNI != "" {
return nil, ErrLinkSNINotSupported
}
dialers, err := l.dialersFor(url, info) dialers, err := l.dialersFor(url, info)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -31,6 +31,9 @@ func (l *links) newLinkUNIX() *linkUNIX {
} }
func (l *linkUNIX) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { func (l *linkUNIX) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
if options.tlsSNI != "" {
return nil, ErrLinkSNINotSupported
}
addr, err := net.ResolveUnixAddr("unix", url.Path) addr, err := net.ResolveUnixAddr("unix", url.Path)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -87,6 +87,9 @@ func (l *links) newLinkWS() *linkWS {
} }
func (l *linkWS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { func (l *linkWS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
if options.tlsSNI != "" {
return nil, ErrLinkSNINotSupported
}
wsconn, _, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{ wsconn, _, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{
Subprotocols: []string{"ygg-ws"}, Subprotocols: []string{"ygg-ws"},
}) })

View File

@@ -27,6 +27,9 @@ func (l *links) newLinkWSS() *linkWSS {
} }
func (l *linkWSS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { func (l *linkWSS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
if options.tlsSNI != "" {
return nil, ErrLinkSNINotSupported
}
wsconn, _, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{ wsconn, _, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{
Subprotocols: []string{"ygg-ws"}, Subprotocols: []string{"ygg-ws"},
}) })

View File

@@ -2,9 +2,7 @@ package ipv6rwc
import ( import (
"crypto/ed25519" "crypto/ed25519"
"errors" "net/netip"
"fmt"
"net"
"sync" "sync"
"time" "time"
@@ -19,15 +17,6 @@ import (
const keyStoreTimeout = 2 * time.Minute const keyStoreTimeout = 2 * time.Minute
/*
// Out-of-band packet types
const (
typeKeyDummy = iota // nolint:deadcode,varcheck
typeKeyLookup
typeKeyResponse
)
*/
type keyArray [ed25519.PublicKeySize]byte type keyArray [ed25519.PublicKeySize]byte
type keyStore struct { type keyStore struct {
@@ -40,6 +29,7 @@ type keyStore struct {
addrBuffer map[address.Address]*buffer addrBuffer map[address.Address]*buffer
subnetToInfo map[address.Subnet]*keyInfo subnetToInfo map[address.Subnet]*keyInfo
subnetBuffer map[address.Subnet]*buffer subnetBuffer map[address.Subnet]*buffer
tunnelHelper TunnelHelper
mtu uint64 mtu uint64
} }
@@ -59,10 +49,6 @@ func (k *keyStore) init(c *core.Core) {
k.core = c k.core = c
k.address = *address.AddrForKey(k.core.PublicKey()) k.address = *address.AddrForKey(k.core.PublicKey())
k.subnet = *address.SubnetForKey(k.core.PublicKey()) k.subnet = *address.SubnetForKey(k.core.PublicKey())
/*if err := k.core.SetOutOfBandHandler(k.oobHandler); err != nil {
err = fmt.Errorf("tun.core.SetOutOfBandHander: %w", err)
panic(err)
}*/
k.core.SetPathNotify(func(key ed25519.PublicKey) { k.core.SetPathNotify(func(key ed25519.PublicKey) {
k.update(key) k.update(key)
}) })
@@ -182,49 +168,10 @@ func (k *keyStore) resetTimeout(info *keyInfo) {
}) })
} }
/*
func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { // nolint:unused
if len(data) != 1+ed25519.SignatureSize {
return
}
sig := data[1:]
switch data[0] {
case typeKeyLookup:
snet := *address.SubnetForKey(toKey)
if snet == k.subnet && ed25519.Verify(fromKey, toKey[:], sig) {
// This is looking for at least our subnet (possibly our address)
// Send a response
k.sendKeyResponse(fromKey)
}
case typeKeyResponse:
// TODO keep a list of something to match against...
// Ignore the response if it doesn't match anything of interest...
if ed25519.Verify(fromKey, toKey[:], sig) {
k.update(fromKey)
}
}
}
*/
func (k *keyStore) sendKeyLookup(partial ed25519.PublicKey) { func (k *keyStore) sendKeyLookup(partial ed25519.PublicKey) {
/*
sig := ed25519.Sign(k.core.PrivateKey(), partial[:])
bs := append([]byte{typeKeyLookup}, sig...)
//_ = k.core.SendOutOfBand(partial, bs)
_ = bs
*/
k.core.SendLookup(partial) k.core.SendLookup(partial)
} }
/*
func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) { // nolint:unused
sig := ed25519.Sign(k.core.PrivateKey(), dest[:])
bs := append([]byte{typeKeyResponse}, sig...)
//_ = k.core.SendOutOfBand(dest, bs)
_ = bs
}
*/
func (k *keyStore) readPC(p []byte) (int, error) { func (k *keyStore) readPC(p []byte) (int, error) {
buf := make([]byte, k.core.MTU(), 65535) buf := make([]byte, k.core.MTU(), 65535)
for { for {
@@ -240,16 +187,22 @@ func (k *keyStore) readPC(p []byte) (int, error) {
if len(bs) == 0 { if len(bs) == 0 {
continue continue
} }
if bs[0]&0xf0 != 0x60 { ip4 := bs[0]&0xf0 == 0x40
continue // not IPv6 ip6 := bs[0]&0xf0 == 0x60
} switch {
if len(bs) < 40 { case !ip4 && !ip6:
continue
case ip6 && len(bs) < 40:
continue
case ip4 && len(bs) < 20:
continue continue
} }
k.mutex.Lock() k.mutex.Lock()
mtu := int(k.mtu) mtu := int(k.mtu)
th := k.tunnelHelper
k.mutex.Unlock() k.mutex.Unlock()
if len(bs) > mtu { switch {
case ip6 && len(bs) > mtu:
// Using bs would make it leak off the stack, so copy to buf // Using bs would make it leak off the stack, so copy to buf
buf := make([]byte, 512) buf := make([]byte, 512)
cn := copy(buf, bs) cn := copy(buf, bs)
@@ -261,51 +214,87 @@ func (k *keyStore) readPC(p []byte) (int, error) {
_, _ = k.writePC(packet) _, _ = k.writePC(packet)
} }
continue continue
case len(bs) > mtu:
continue
} }
var srcAddr, dstAddr address.Address var srcAddr, dstAddr address.Address
var srcSubnet, dstSubnet address.Subnet var srcSubnet, dstSubnet address.Subnet
copy(srcAddr[:], bs[8:]) var addrlen int
copy(dstAddr[:], bs[24:]) switch {
copy(srcSubnet[:], bs[8:]) case ip4:
copy(dstSubnet[:], bs[24:]) copy(srcAddr[:], bs[12:16])
if dstAddr != k.address && dstSubnet != k.subnet { addrlen = 4
continue // bad local address/subnet case ip6:
copy(srcAddr[:], bs[8:])
copy(srcSubnet[:], bs[8:])
copy(dstAddr[:], bs[24:])
copy(dstSubnet[:], bs[24:])
addrlen = 16
} }
info := k.update(ed25519.PublicKey(from.(iwt.Addr))) srcKey := ed25519.PublicKey(from.(iwt.Addr))
if srcAddr != info.address && srcSubnet != info.subnet { info := k.update(srcKey)
continue // bad remote address/subnet switch {
case ip6 && (srcAddr == info.address || srcSubnet == info.subnet):
return copy(p, bs), nil
case ip4, ip6:
if th == nil {
continue
}
addr, ok := netip.AddrFromSlice(srcAddr[:addrlen])
if !ok || !th.InboundAllowed(addr, srcKey) {
continue
}
} }
n = copy(p, bs) return copy(p, bs), nil
return n, nil
} }
} }
func (k *keyStore) writePC(bs []byte) (int, error) { func (k *keyStore) writePC(bs []byte) (int, error) {
if bs[0]&0xf0 != 0x60 { if len(bs) == 0 {
return 0, errors.New("not an IPv6 packet") // not IPv6 return 0, nil
} }
if len(bs) < 40 { ip4 := bs[0]&0xf0 == 0x40
strErr := fmt.Sprint("undersized IPv6 packet, length: ", len(bs)) ip6 := bs[0]&0xf0 == 0x60
return 0, errors.New(strErr) switch {
case !ip4 && !ip6:
return len(bs), nil
case ip6 && len(bs) < 40:
return len(bs), nil
case ip4 && len(bs) < 20:
return len(bs), nil
} }
var srcAddr, dstAddr address.Address var dstAddr address.Address
var srcSubnet, dstSubnet address.Subnet var dstSubnet address.Subnet
copy(srcAddr[:], bs[8:]) var addrlen int
copy(dstAddr[:], bs[24:]) switch {
copy(srcSubnet[:], bs[8:]) case ip4:
copy(dstSubnet[:], bs[24:]) copy(dstAddr[:], bs[16:20])
if srcAddr != k.address && srcSubnet != k.subnet { addrlen = 4
// This happens all the time due to link-local traffic case ip6:
// Don't send back an error, just drop it copy(dstAddr[:], bs[24:40])
strErr := fmt.Sprint("incorrect source address: ", net.IP(srcAddr[:]).String()) copy(dstSubnet[:], bs[24:40])
return 0, errors.New(strErr) addrlen = 16
} }
if dstAddr.IsValid() { switch {
case dstAddr.IsValid():
k.sendToAddress(dstAddr, bs) k.sendToAddress(dstAddr, bs)
} else if dstSubnet.IsValid() { case dstSubnet.IsValid():
k.sendToSubnet(dstSubnet, bs) k.sendToSubnet(dstSubnet, bs)
} else { default:
return 0, errors.New("invalid destination address") k.mutex.Lock()
th := k.tunnelHelper
k.mutex.Unlock()
if th == nil {
return len(bs), nil
}
addr, ok := netip.AddrFromSlice(dstAddr[:addrlen])
if !ok {
return len(bs), nil
}
if key := th.OutboundAllowed(addr); key != nil && len(key) == ed25519.PublicKeySize {
return k.core.WriteTo(bs, iwt.Addr(key))
}
return len(bs), nil
} }
return len(bs), nil return len(bs), nil
} }
@@ -366,3 +355,14 @@ func (rwc *ReadWriteCloser) Close() error {
rwc.core.Stop() rwc.core.Stop()
return err return err
} }
func (rwc *ReadWriteCloser) SetTunnelHelper(h TunnelHelper) {
rwc.mutex.Lock()
defer rwc.mutex.Unlock()
rwc.tunnelHelper = h
}
type TunnelHelper interface {
InboundAllowed(srcip netip.Addr, src ed25519.PublicKey) bool
OutboundAllowed(dstip netip.Addr) ed25519.PublicKey
}

View File

@@ -125,6 +125,7 @@ func (tun *TunAdapter) _start() error {
if tun.config.name == "none" || tun.config.name == "dummy" { if tun.config.name == "none" || tun.config.name == "dummy" {
tun.log.Debugln("Not starting TUN as ifname is none or dummy") tun.log.Debugln("Not starting TUN as ifname is none or dummy")
tun.isEnabled = false tun.isEnabled = false
go tun.queue()
go tun.write() go tun.write()
return nil return nil
} }