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
4 changes: 2 additions & 2 deletions core/nylon_wireguard.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ listen_port=%d
}
}

if !n.NoNetConfigure {
if !n.NoNetConfigure && !n.NoTun {
for _, addr := range n.GetRouter(n.LocalCfg.Id).Addresses {
err := ConfigureAlias(n.Log, itfName, addr)
if err != nil {
Expand Down Expand Up @@ -235,7 +235,7 @@ func (n *Nylon) syncWireGuardEndpoints() error {
}

func (n *Nylon) SyncSystemState() error {
if n.NoNetConfigure {
if n.NoNetConfigure || n.NoTun {
return nil
}
return errors.Join(n.syncAliases(), n.syncSystemRoutes())
Expand Down
19 changes: 14 additions & 5 deletions core/sys_physical.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ import (
func NewWireGuardDevice(n *Nylon) (dev *device.Device, tunDevice tun.Device, realItf string, err error) {
itfName := n.InterfaceName // attempt to name the interface

if runtime.GOOS == "darwin" {
if runtime.GOOS == "darwin" && !n.NoTun {
itfName = "utun"
}

tdev, err := tun.CreateTUN(itfName, device.DefaultMTU)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to create TUN: %v. Check if an interface with the name nylon exists already", err)
var tdev tun.Device
if n.NoTun {
tdev = tun.NewDummyDevice(itfName)
} else {
tdev, err = tun.CreateTUN(itfName, device.DefaultMTU)
if err != nil {
return nil, nil, "", fmt.Errorf("failed to create TUN: %v. Check if an interface with the name nylon exists already", err)
}
}
realInterfaceName, err := tdev.Name()
if err == nil {
Expand Down Expand Up @@ -65,7 +70,11 @@ func NewWireGuardDevice(n *Nylon) (dev *device.Device, tunDevice tun.Device, rea
}()
}

n.Log.Info("Created WireGuard interface", "name", itfName)
if n.NoTun {
n.Log.Info("Created userspace-only WireGuard device", "name", itfName)
} else {
n.Log.Info("Created WireGuard interface", "name", itfName)
}
return dev, tdev, itfName, nil
}

Expand Down
13 changes: 11 additions & 2 deletions core/sys_virtual.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ func NewWireGuardDevice(n *Nylon) (dev *device.Device, tunDevice tun.Device, rea
itfName := "nylon-vn"

bind := vn.Bind(n.LocalCfg.Id)
tdev := vn.Tun(n.LocalCfg.Id)
var tdev tun.Device
if n.NoTun {
tdev = tun.NewDummyDevice(itfName)
} else {
tdev = vn.Tun(n.LocalCfg.Id)
}

wgLog := n.Log.With("module", log.ScopePolyamide)

Expand All @@ -47,7 +52,11 @@ func NewWireGuardDevice(n *Nylon) (dev *device.Device, tunDevice tun.Device, rea
},
})

n.Log.Info("Created WireGuard interface", "name", itfName)
if n.NoTun {
n.Log.Info("Created userspace-only WireGuard device", "name", itfName)
} else {
n.Log.Info("Created WireGuard interface", "name", itfName)
}
return dev, tdev, itfName, nil
}

Expand Down
1 change: 1 addition & 0 deletions docs/reference/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ port: 57175 # UDP port nylon listens on
# --- Optional fields below ---

use_system_routing: false # if true, all peer packets exit via the TUN interface
no_tun: false # if true, run as a userspace-only relay without creating a TUN interface (requires use_system_routing: false)
no_net_configure: false # if true, nylon won't touch system routes or interfaces
log_path: "" # write logs to this file (empty = stderr only)
interface_name: "" # override the interface name (default: "nylon", or utunX on macOS)
Expand Down
3 changes: 2 additions & 1 deletion example/sample-node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ port: 57175 # Default: 57175 - UDP port that Nylon listens on

# the following are optional
use_system_routing: false # Default: false - all packets from peers will come out of the TUN interface
no_tun: false # Default: false - run as a userspace-only relay without creating a TUN interface
no_net_configure: false # Default: false - do not configure system networking at all
log_path: "" # Default: "" - If set, Nylon will log to this file
interface_name: "" # Default: "" - If set, Nylon will use this interface name instead of the default "nylon" or utunx on macOS
Expand All @@ -20,4 +21,4 @@ pre_up: []
pre_down: []
post_up:
- iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -d 192.168.0.0/24 -j MASQUERADE # can be used in conjunction with an advertised prefix
post_down: []
post_down: []
56 changes: 56 additions & 0 deletions integration/routing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,62 @@ func TestInProcessRouting(t *testing.T) {
vh.Stop()
}

func TestInProcessRoutingThroughTUNLessRelay(t *testing.T) {
defer goleak.VerifyNone(t)
vh := &VirtualHarness{}
vh.UntrackedRouting = true
a1 := "192.168.1.1:1234"
vh.NewNode("a", "10.0.0.1/32")
b1 := "192.168.1.2:1234"
vh.NewNode("b", "10.0.0.2/32")
vh.Local[vh.IndexOf("b")].NoTun = true
c1 := "192.168.1.3:1234"
vh.NewNode("c", "10.0.0.3/32")
vh.Central.Graph = []string{
"a, b",
"b, c",
}
vh.Endpoints = map[string]state.NodeId{
a1: "a",
b1: "b",
c1: "c",
}
vh.AddLink(a1, b1)
vh.AddLink(b1, a1)
vh.AddLink(b1, c1)
vh.AddLink(c1, b1)

errs := vh.Start()
defer vh.Stop()

received := make(chan struct{}, 1)
vh.Net.SelfHandler = func(node state.NodeId, src, dst netip.Addr, data []byte) bool {
if node == "c" && src.String() == "10.0.0.1" && dst.String() == "10.0.0.3" && data[0] == 222 {
received <- struct{}{}
}
return true
}

go func() {
for {
select {
case <-vh.Context.Done():
return
case <-time.After(100 * time.Millisecond):
vh.Net.Send("a", "10.0.0.1", "10.0.0.3", []byte{222}, 64)
}
}
}()

select {
case <-received:
case <-time.After(10 * time.Second):
t.Error("timed out waiting for packet through TUN-less relay")
case err := <-errs:
t.Error(err)
}
}

func TestTTL(t *testing.T) {
defer goleak.VerifyNone(t)
vh := &VirtualHarness{}
Expand Down
66 changes: 66 additions & 0 deletions polyamide/tun/dummy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tun

import (
"os"
"sync"
)

// DummyDevice is a userspace-only TUN device. It lets the WireGuard data plane
// operate without creating a host network interface. Reads block until the
// device is closed, while writes are discarded.
type DummyDevice struct {
name string
closed chan struct{}
events chan Event
closeOnce sync.Once
}

func NewDummyDevice(name string) *DummyDevice {
return &DummyDevice{
name: name,
closed: make(chan struct{}),
events: make(chan Event),
}
}

func (d *DummyDevice) File() *os.File {
return nil
}

func (d *DummyDevice) Read(_ [][]byte, _ []int, _ int) (int, error) {
<-d.closed
return 0, os.ErrClosed
}

func (d *DummyDevice) Write(bufs [][]byte, _ int) (int, error) {
select {
case <-d.closed:
return 0, os.ErrClosed
default:
return len(bufs), nil
}
}

func (d *DummyDevice) MTU() (int, error) {
return 1420, nil
}

func (d *DummyDevice) Name() (string, error) {
return d.name, nil
}

func (d *DummyDevice) Events() <-chan Event {
return d.events
}

func (d *DummyDevice) Close() error {
d.closeOnce.Do(func() {
close(d.closed)
close(d.events)
})
return nil
}

func (d *DummyDevice) BatchSize() int {
return 1
}
47 changes: 47 additions & 0 deletions polyamide/tun/dummy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package tun

import (
"errors"
"os"
"testing"
)

func TestDummyDevice(t *testing.T) {
dev := NewDummyDevice("relay")

name, err := dev.Name()
if err != nil {
t.Fatal(err)
}
if name != "relay" {
t.Fatalf("Name() = %q, want relay", name)
}
if mtu, err := dev.MTU(); err != nil || mtu != 1420 {
t.Fatalf("MTU() = %d, %v; want 1420, nil", mtu, err)
}
if n, err := dev.Write([][]byte{{1, 2, 3}}, 0); err != nil || n != 1 {
t.Fatalf("Write() = %d, %v; want 1, nil", n, err)
}

readDone := make(chan error, 1)
go func() {
_, err := dev.Read(nil, nil, 0)
readDone <- err
}()

if err := dev.Close(); err != nil {
t.Fatal(err)
}
if err := dev.Close(); err != nil {
t.Fatal(err)
}
if err := <-readDone; !errors.Is(err, os.ErrClosed) {
t.Fatalf("Read() error = %v, want os.ErrClosed", err)
}
if _, ok := <-dev.Events(); ok {
t.Fatal("Events channel remained open after Close")
}
if _, err := dev.Write([][]byte{{1}}, 0); !errors.Is(err, os.ErrClosed) {
t.Fatalf("Write() error = %v, want os.ErrClosed", err)
}
}
1 change: 1 addition & 0 deletions state/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type LocalCfg struct {
Port uint16 // Address that the data plane can be accessed by
Dist *LocalDistributionCfg `yaml:",omitempty"` // distribution configuration
UseSystemRouting bool `yaml:"use_system_routing,omitempty"` // all packets from peers will come out of the TUN interface
NoTun bool `yaml:"no_tun,omitempty"` // run without creating a TUN interface (relay-only mode)
NoNetConfigure bool `yaml:"no_net_configure,omitempty"` // do not configure system networking at all
DnsResolvers []string `yaml:"dns_resolvers,omitempty"` // DNS resolvers used for endpoints and config repositories
InterfaceName string `yaml:"interface_name,omitempty"` // the name of the nylon interface
Expand Down
3 changes: 3 additions & 0 deletions state/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ func NodeConfigValidator(central *CentralCfg, node *LocalCfg) error {
if node.Key == [32]byte{} {
return fmt.Errorf("private key must not be empty")
}
if node.NoTun && node.UseSystemRouting {
return fmt.Errorf("no_tun cannot be used with use_system_routing")
}
if node.InterfaceName != "" {
err = NameValidator(node.InterfaceName)
if err != nil {
Expand Down
13 changes: 13 additions & 0 deletions state/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ func TestNodeConfigValidator_DnsResolver(t *testing.T) {
}))
}

func TestNodeConfigValidator_TunlessMode(t *testing.T) {
base := LocalCfg{
Id: "relay",
Port: 57175,
Key: [32]byte{1},
NoTun: true,
}
assert.NoError(t, NodeConfigValidator(nil, &base))

base.UseSystemRouting = true
assert.ErrorContains(t, NodeConfigValidator(nil, &base), "no_tun cannot be used with use_system_routing")
}

func TestCentralConfigValidator_OverlappingPrefix(t *testing.T) {
cfg := &CentralCfg{
Routers: []RouterCfg{
Expand Down
Loading