From 2a086d07e70276697dd8cdb917398f66d0591881 Mon Sep 17 00:00:00 2001 From: netbyte Date: Tue, 26 Jul 2022 22:10:38 +0800 Subject: [PATCH] init --- .gitignore | 2 + .travis.yml | 12 ++ CONTRIBUTORS | 15 +++ LICENSE | 27 ++++ Makefile | 23 ++++ README.md | 224 +++++++++++++++++++++++++++++++ doc.go | 4 + go.mod | 13 ++ go.sum | 8 ++ if.go | 80 +++++++++++ if_linux.go | 30 +++++ params_darwin.go | 32 +++++ params_linux.go | 45 +++++++ params_others.go | 11 ++ params_windows.go | 25 ++++ syscalls_darwin.go | 270 ++++++++++++++++++++++++++++++++++++++ syscalls_darwin_go1.11.go | 9 ++ syscalls_darwin_legacy.go | 10 ++ syscalls_linux.go | 83 ++++++++++++ syscalls_linux_go1.11.go | 27 ++++ syscalls_linux_legacy.go | 26 ++++ syscalls_other.go | 9 ++ syscalls_windows.go | 41 ++++++ 23 files changed, 1026 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTORS create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 doc.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 if.go create mode 100644 if_linux.go create mode 100644 params_darwin.go create mode 100644 params_linux.go create mode 100644 params_others.go create mode 100644 params_windows.go create mode 100644 syscalls_darwin.go create mode 100644 syscalls_darwin_go1.11.go create mode 100644 syscalls_darwin_legacy.go create mode 100644 syscalls_linux.go create mode 100644 syscalls_linux_go1.11.go create mode 100644 syscalls_linux_legacy.go create mode 100644 syscalls_other.go create mode 100644 syscalls_windows.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22268fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +water.test +water.test.exe diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2ef7198 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: go +go: + - "1.18" +go_import_path: github.com/net-byte/water +install: go get -u golang.org/x/lint/golint +script: make ci + +matrix: + include: + - os: linux + dist: xenial + - os: osx diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..ccae992 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,15 @@ +Song Gao +Harshal Sheth +KOJIMA Takanori +Sean Purser-Haskell +daregod +Lucus Lee +Arroyo Networks, LLC +Tony Lu +ajee cai +yinheli +Paul Querna +Cuong Manh Le +Neil Alexander +Dmitry Shihovtsev +Yifan Gu [https://github.com/gyf304] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..65deac8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016, Song Gao +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of water nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..70d55bd --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.phony: default ci test lint vet gofmt + + +default: + echo 'This make file is for CI.' + exit 1 + +ci: test lint vet gofmt + +test: water.test + sudo ./water.test -test.v + +lint: + golint -set_exit_status + +vet: + go vet . + +gofmt: + gofmt -s -e -l . + +water.test: *.go + go test -c diff --git a/README.md b/README.md new file mode 100644 index 0000000..06ac645 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# water + +`water` is a native Go library for [TUN/TAP](http://en.wikipedia.org/wiki/TUN/TAP) interfaces. + +`water` is designed to be simple and efficient. It + +* wraps almost only syscalls and uses only Go standard types; +* exposes standard interfaces; plays well with standard packages like `io`, `bufio`, etc.. +* does not handle memory management (allocating/destructing slice). It's up to user to decide whether/how to reuse buffers. + +## Supported Platforms + +* Linux +* Windows (based on wintun TUN only) +* macOS (point-to-point TUN only) + +## Installation +``` +go get -u github.com/net-byte/water +``` + +## Example + +### TAP on Linux: + +```go +package main + +import ( + "log" + + "github.com/songgao/packets/ethernet" + "github.com/net-byte/water" +) + +func main() { + config := water.Config{ + DeviceType: water.TAP, + } + config.Name = "O_O" + + ifce, err := water.New(config) + if err != nil { + log.Fatal(err) + } + var frame ethernet.Frame + + for { + frame.Resize(1500) + n, err := ifce.Read([]byte(frame)) + if err != nil { + log.Fatal(err) + } + frame = frame[:n] + log.Printf("Dst: %s\n", frame.Destination()) + log.Printf("Src: %s\n", frame.Source()) + log.Printf("Ethertype: % x\n", frame.Ethertype()) + log.Printf("Payload: % x\n", frame.Payload()) + } +} +``` + +This piece of code creates a `TAP` interface, and prints some header information for every frame. After pull up the `main.go`, you'll need to bring up the interface and assign an IP address. All of these need root permission. + +```bash +sudo go run main.go +``` + +In a new terminal: + +```bash +sudo ip addr add 10.1.0.10/24 dev O_O +sudo ip link set dev O_O up +``` + +Wait until the output `main.go` terminal, try sending some ICMP broadcast message: +```bash +ping -c1 -b 10.1.0.255 +``` + +You'll see output containing the IPv4 ICMP frame: +``` +2016/10/24 03:18:16 Dst: ff:ff:ff:ff:ff:ff +2016/10/24 03:18:16 Src: 72:3c:fc:29:1c:6f +2016/10/24 03:18:16 Ethertype: 08 00 +2016/10/24 03:18:16 Payload: 45 00 00 54 00 00 40 00 40 01 25 9f 0a 01 00 0a 0a 01 00 ff 08 00 01 c1 08 49 00 01 78 7d 0d 58 00 00 00 00 a2 4c 07 00 00 00 00 00 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 +``` + +### TUN on macOS + +```go +package main + +import ( + "log" + + "github.com/net-byte/water" +) + +func main() { + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + log.Fatal(err) + } + + log.Printf("Interface Name: %s\n", ifce.Name()) + + packet := make([]byte, 2000) + for { + n, err := ifce.Read(packet) + if err != nil { + log.Fatal(err) + } + log.Printf("Packet Received: % x\n", packet[:n]) + } +} +``` + +Run it! + +```bash +$ sudo go run main.go +``` + +This is a point-to-point only interface. Use `ifconfig` to see its attributes. You need to bring it up and assign IP addresses (apparently replace `utun2` if needed): + +```bash +$ sudo ifconfig utun2 10.1.0.10 10.1.0.20 up +``` + +Now send some ICMP packets to the interface: + +```bash +$ ping 10.1.0.20 +``` + +You'd see the ICMP packets printed out: + +``` +2017/03/20 21:17:30 Interface Name: utun2 +2017/03/20 21:17:40 Packet Received: 45 00 00 54 e9 1d 00 00 40 01 7d 6c 0a 01 00 0a 0a 01 00 14 08 00 ee 04 21 15 00 00 58 d0 a9 64 00 08 fb a5 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 +``` + +#### Caveats + +1. Only Point-to-Point user TUN devices are supported. TAP devices are *not* supported natively by macOS. +2. Custom interface names are not supported by macOS. Interface names are automatically generated serially, using the `utun<#>` naming convention. + +### TAP on Windows: + +To use it with windows, you will need to install a [wintun driver](https://www.wintun.net/) for windows. + +It's compatible with the Linux code. + +```go +package main + +import ( + "log" + + "github.com/net-byte/water" +) + +func main() { + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + log.Fatal(err) + } + + log.Printf("Interface Name: %s\n", ifce.Name()) + + packet := make([]byte, 2000) + for { + n, err := ifce.Read(packet) + if err != nil { + log.Fatal(err) + } + log.Printf("Packet Received: % x\n", packet[:n]) + } +} +``` + +Same as Linux version, but you don't need to bring up the device by hand, the only thing you need is to assign an IP address to it. + +```dos +go run main.go +``` + +It will output a lot of lines because of some windows services and dhcp. +You will need admin right to assign IP. + +In a new cmd (admin right): + +```dos +# Replace with your device name, it can be achieved by ifce.Name(). +netsh interface ip set address name="Ehternet 2" source=static addr=10.1.0.10 mask=255.255.255.0 gateway=none +``` + +The `main.go` terminal should be silenced after IP assignment, try sending some ICMP broadcast message: + +```dos +ping 10.1.0.255 +``` + +You'll see output containing the IPv4 ICMP frame same as the Linux version. + +#### Specifying interface name + +If you are going to use multiple TUN devices on the Windows, there is a way to specify an interface name to select the exact device that you need: + +```go + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + PlatformSpecificParams: water.PlatformSpecificParams{ + InterfaceName: "Ethernet 3", + Network: "172.16.1.10/24", + }, + }) +``` + diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..287412b --- /dev/null +++ b/doc.go @@ -0,0 +1,4 @@ +// Package water is a simple TUN/TAP interface library that efficiently works +// with standard packages like io, bufio, etc.. Use waterutil with it to work +// with TUN/TAP packets/frames. +package water diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aad645d --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/net-byte/water + +go 1.18 + +require ( + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f + golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 +) + +require ( + golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect + golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..29c216f --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= +golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= +golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 h1:vDy//hdR+GnROE3OdYbQKt9rdtNdHkDtONvpRwmls/0= +golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U= diff --git a/if.go b/if.go new file mode 100644 index 0000000..4023a1a --- /dev/null +++ b/if.go @@ -0,0 +1,80 @@ +package water + +import ( + "errors" + "io" +) + +// Interface is a TUN/TAP interface. +// +// MultiQueue(Linux kernel > 3.8): With MultiQueue enabled, user should hold multiple +// interfaces to send/receive packet in parallel. +// Kernel document about MultiQueue: https://www.kernel.org/doc/Documentation/networking/tuntap.txt +type Interface struct { + isTAP bool + io.ReadWriteCloser + name string +} + +// DeviceType is the type for specifying device types. +type DeviceType int + +// TUN and TAP device types. +const ( + _ = iota + TUN + TAP +) + +// Config defines parameters required to create a TUN/TAP interface. It's only +// used when the device is initialized. A zero-value Config is a valid +// configuration. +type Config struct { + // DeviceType specifies whether the device is a TUN or TAP interface. A + // zero-value is treated as TUN. + DeviceType DeviceType + + // PlatformSpecificParams defines parameters that differ on different + // platforms. See comments for the type for more details. + PlatformSpecificParams +} + +func defaultConfig() Config { + return Config{ + DeviceType: TUN, + PlatformSpecificParams: defaultPlatformSpecificParams(), + } +} + +var zeroConfig Config + +// New creates a new TUN/TAP interface using config. +func New(config Config) (ifce *Interface, err error) { + if zeroConfig == config { + config = defaultConfig() + } + if config.PlatformSpecificParams == zeroConfig.PlatformSpecificParams { + config.PlatformSpecificParams = defaultPlatformSpecificParams() + } + switch config.DeviceType { + case TUN, TAP: + return openDev(config) + default: + return nil, errors.New("unknown device type") + } +} + +// IsTUN returns true if ifce is a TUN interface. +func (ifce *Interface) IsTUN() bool { + return !ifce.isTAP +} + +// IsTAP returns true if ifce is a TAP interface. +func (ifce *Interface) IsTAP() bool { + return ifce.isTAP +} + +// Name returns the interface name of ifce, e.g. tun0, tap1, tun0, etc.. +func (ifce *Interface) Name() string { + return ifce.name +} diff --git a/if_linux.go b/if_linux.go new file mode 100644 index 0000000..227e8a8 --- /dev/null +++ b/if_linux.go @@ -0,0 +1,30 @@ +package water + +import ( + "fmt" +) + +// NewTAP creates a new TAP interface whose name is ifName. If ifName is empty, a +// default name (tap0, tap1, ... ) will be assigned. ifName should not exceed +// 16 bytes. TAP interfaces are not supported on darwin. +// ifName cannot be specified on windows, you will need ifce.Name() to use some cmds. +// +// Deprecated: This function may be removed in the future. Please use New() instead. +func NewTAP(ifName string) (ifce *Interface, err error) { + fmt.Println("Deprecated: NewTAP(..) may be removed in the future. Please use New() instead.") + config := Config{DeviceType: TAP} + config.Name = ifName + return openDev(config) +} + +// NewTUN creates a new TUN interface whose name is ifName. If ifName is empty, a +// default name (tap0, tap1, ... ) will be assigned. ifName should not exceed +// ifName cannot be specified on windows, you will need ifce.Name() to use some cmds. +// +// Deprecated: This function will be removed in the future. Please use New() instead. +func NewTUN(ifName string) (ifce *Interface, err error) { + fmt.Println("Deprecated: NewTUN(..) may be removed in the future. Please use New() instead.") + config := Config{DeviceType: TUN} + config.Name = ifName + return openDev(config) +} diff --git a/params_darwin.go b/params_darwin.go new file mode 100644 index 0000000..f33a07a --- /dev/null +++ b/params_darwin.go @@ -0,0 +1,32 @@ +package water + +// MacOSDriverProvider enumerates possible MacOS TUN/TAP implementations +type MacOSDriverProvider int + +const ( + // MacOSDriverSystem refers to the default P2P driver + MacOSDriverSystem MacOSDriverProvider = 0 + // MacOSDriverTunTapOSX refers to the third-party tuntaposx driver + // see https://sourceforge.net/p/tuntaposx + MacOSDriverTunTapOSX MacOSDriverProvider = 1 +) + +// PlatformSpecificParams defines parameters in Config that are specific to +// macOS. A zero-value of such type is valid, yielding an interface +// with OS defined name. +// Currently it is not possible to set the interface name in macOS. +type PlatformSpecificParams struct { + // Name is the name for the interface to be used. + // + // For TunTapOSXDriver, it should be something like "tap0". + // For SystemDriver, the name should match `utun[0-9]+`, e.g. utun233 + Name string + + // Driver should be set if an alternative driver is desired + // e.g. TunTapOSXDriver + Driver MacOSDriverProvider +} + +func defaultPlatformSpecificParams() PlatformSpecificParams { + return PlatformSpecificParams{} +} diff --git a/params_linux.go b/params_linux.go new file mode 100644 index 0000000..34e9898 --- /dev/null +++ b/params_linux.go @@ -0,0 +1,45 @@ +package water + +// DevicePermissions determines the owner and group owner for the newly created +// interface. +type DevicePermissions struct { + // Owner is the ID of the user which will be granted ownership of the + // device. If set to a negative value, the owner value will not be + // changed. By default, Linux sets the owner to -1, which allows any user. + Owner uint + + // Group is the ID of the group which will be granted access to the device. + // If set to a negative value, the group value will not be changed. By + // default, Linux sets the group to -1, which allows any group. + Group uint +} + +// PlatformSpecificParams defines parameters in Config that are specific to +// Linux. A zero-value of such type is valid, yielding an interface +// with OS defined name. +type PlatformSpecificParams struct { + // Name is the name to be set for the interface to be created. This overrides + // the default name assigned by OS such as tap0 or tun0. A zero-value of this + // field, i.e. an empty string, indicates that the default name should be + // used. + Name string + + // Persist specifies whether persistence mode for the interface device + // should be enabled or disabled. + Persist bool + + // Permissions, if non-nil, specifies the owner and group owner for the + // interface. A zero-value of this field, i.e. nil, indicates that no + // changes to owner or group will be made. + Permissions *DevicePermissions + + // MultiQueue specifies whether the multiqueue flag should be set on the + // interface. From version 3.8, Linux supports multiqueue tuntap which can + // uses multiple file descriptors (queues) to parallelize packets sending + // or receiving. + MultiQueue bool +} + +func defaultPlatformSpecificParams() PlatformSpecificParams { + return PlatformSpecificParams{} +} diff --git a/params_others.go b/params_others.go new file mode 100644 index 0000000..c5c4f6e --- /dev/null +++ b/params_others.go @@ -0,0 +1,11 @@ +// +build !linux,!darwin,!windows + +package water + +// PlatformSpeficParams +type PlatformSpecificParams struct { +} + +func defaultPlatformSpecificParams() PlatformSpecificParams { + return PlatformSpecificParams{} +} diff --git a/params_windows.go b/params_windows.go new file mode 100644 index 0000000..7a193e3 --- /dev/null +++ b/params_windows.go @@ -0,0 +1,25 @@ +package water + +// PlatformSpecificParams defines parameters in Config that are specific to +// Windows. A zero-value of such type is valid. +type PlatformSpecificParams struct { + InterfaceName string + // Network is required when creating a TUN interface. The library will call + // net.ParseCIDR() to parse this string into LocalIP, RemoteNetaddr, + // RemoteNetmask. The underlying driver will need those to generate ARP + // response to Windows kernel, to emulate an TUN interface. + // Please note that it cannot perceive the IP changes caused by DHCP, user + // configuration to the adapter and etc,. If IP changed, please reconfigure + // the adapter using syscall, just like openDev(). + // For detail, please refer + // https://github.com/OpenVPN/tap-windows6/blob/master/src/device.c#L431 + // and https://github.com/songgao/water/pull/13#issuecomment-270341777 + Network string +} + +func defaultPlatformSpecificParams() PlatformSpecificParams { + return PlatformSpecificParams{ + InterfaceName: "wintun", + Network: "172.16.1.10/24", + } +} diff --git a/syscalls_darwin.go b/syscalls_darwin.go new file mode 100644 index 0000000..0b27349 --- /dev/null +++ b/syscalls_darwin.go @@ -0,0 +1,270 @@ +package water + +import ( + "errors" + "fmt" + "io" + "math" + "os" + "strconv" + "strings" + "sync" + "syscall" + "unsafe" +) + +const appleUTUNCtl = "com.apple.net.utun_control" + +/* + * From ioctl.h: + * #define IOCPARM_MASK 0x1fff // parameter length, at most 13 bits + * ... + * #define IOC_OUT 0x40000000 // copy out parameters + * #define IOC_IN 0x80000000 // copy in parameters + * #define IOC_INOUT (IOC_IN|IOC_OUT) + * ... + * #define _IOC(inout,group,num,len) \ + * (inout | ((len & IOCPARM_MASK) << 16) | ((group) << 8) | (num)) + * ... + * #define _IOWR(g,n,t) _IOC(IOC_INOUT, (g), (n), sizeof(t)) + * + * From kern_control.h: + * #define CTLIOCGINFO _IOWR('N', 3, struct ctl_info) // get id from name + * + */ + +const appleCTLIOCGINFO = (0x40000000 | 0x80000000) | ((100 & 0x1fff) << 16) | uint32(byte('N'))<<8 | 3 + +/* + * #define _IOW(g,n,t) _IOC(IOC_IN, (g), (n), sizeof(t)) + * #define TUNSIFMODE _IOW('t', 94, int) + */ +const appleTUNSIFMODE = (0x80000000) | ((4 & 0x1fff) << 16) | uint32(byte('t'))<<8 | 94 + +/* + * struct sockaddr_ctl { + * u_char sc_len; // depends on size of bundle ID string + * u_char sc_family; // AF_SYSTEM + * u_int16_t ss_sysaddr; // AF_SYS_KERNCONTROL + * u_int32_t sc_id; // Controller unique identifier + * u_int32_t sc_unit; // Developer private unit number + * u_int32_t sc_reserved[5]; + * }; + */ +type sockaddrCtl struct { + scLen uint8 + scFamily uint8 + ssSysaddr uint16 + scID uint32 + scUnit uint32 + scReserved [5]uint32 +} + +var sockaddrCtlSize uintptr = 32 + +func openDev(config Config) (ifce *Interface, err error) { + if config.Driver == MacOSDriverTunTapOSX { + return openDevTunTapOSX(config) + } + if config.Driver == MacOSDriverSystem { + return openDevSystem(config) + } + return nil, errors.New("unrecognized driver") +} + +// openDevSystem opens tun device on system +func openDevSystem(config Config) (ifce *Interface, err error) { + if config.DeviceType != TUN { + return nil, errors.New("only tun is implemented for SystemDriver, use TunTapOSXDriver for tap") + } + + ifIndex := -1 + if config.Name != "" { + const utunPrefix = "utun" + if !strings.HasPrefix(config.Name, utunPrefix) { + return nil, fmt.Errorf("Interface name must be utun[0-9]+") + } + ifIndex, err = strconv.Atoi(config.Name[len(utunPrefix):]) + if err != nil || ifIndex < 0 || ifIndex > math.MaxUint32-1 { + return nil, fmt.Errorf("Interface name must be utun[0-9]+") + } + } + + var fd int + // Supposed to be socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL), but ... + // + // In sys/socket.h: + // #define PF_SYSTEM AF_SYSTEM + // + // In sys/sys_domain.h: + // #define SYSPROTO_CONTROL 2 /* kernel control protocol */ + if fd, err = syscall.Socket(syscall.AF_SYSTEM, syscall.SOCK_DGRAM, 2); err != nil { + return nil, fmt.Errorf("error in syscall.Socket: %v", err) + } + + var ctlInfo = &struct { + ctlID uint32 + ctlName [96]byte + }{} + copy(ctlInfo.ctlName[:], []byte(appleUTUNCtl)) + + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(appleCTLIOCGINFO), uintptr(unsafe.Pointer(ctlInfo))); errno != 0 { + err = errno + return nil, fmt.Errorf("error in syscall.Syscall(syscall.SYS_IOCTL, ...): %v", err) + } + + addrP := unsafe.Pointer(&sockaddrCtl{ + scLen: uint8(sockaddrCtlSize), + scFamily: syscall.AF_SYSTEM, + + /* #define AF_SYS_CONTROL 2 */ + ssSysaddr: 2, + + scID: ctlInfo.ctlID, + scUnit: uint32(ifIndex) + 1, + }) + if _, _, errno := syscall.RawSyscall(syscall.SYS_CONNECT, uintptr(fd), uintptr(addrP), uintptr(sockaddrCtlSize)); errno != 0 { + err = errno + return nil, fmt.Errorf("error in syscall.RawSyscall(syscall.SYS_CONNECT, ...): %v", err) + } + + var ifName struct { + name [16]byte + } + ifNameSize := uintptr(16) + if _, _, errno := syscall.Syscall6(syscall.SYS_GETSOCKOPT, uintptr(fd), + 2, /* #define SYSPROTO_CONTROL 2 */ + 2, /* #define UTUN_OPT_IFNAME 2 */ + uintptr(unsafe.Pointer(&ifName)), + uintptr(unsafe.Pointer(&ifNameSize)), 0); errno != 0 { + err = errno + return nil, fmt.Errorf("error in syscall.Syscall6(syscall.SYS_GETSOCKOPT, ...): %v", err) + } + + if err = setNonBlock(fd); err != nil { + return nil, fmt.Errorf("setting non-blocking error") + } + + return &Interface{ + isTAP: false, + name: string(ifName.name[:ifNameSize-1 /* -1 is for \0 */]), + ReadWriteCloser: &tunReadCloser{ + f: os.NewFile(uintptr(fd), string(ifName.name[:])), + }, + }, nil +} + +// openDevTunTapOSX opens tun / tap device, assuming tuntaposx is installed +func openDevTunTapOSX(config Config) (ifce *Interface, err error) { + var fd int + var socketFD int + + if config.DeviceType == TAP && !strings.HasPrefix(config.Name, "tap") { + return nil, errors.New("device name does not start with tap when creating a tap device") + } + if config.DeviceType == TUN && !strings.HasPrefix(config.Name, "tun") { + return nil, errors.New("device name does not start with tun when creating a tun device") + } + if config.DeviceType != TAP && config.DeviceType != TUN { + return nil, errors.New("unsupported DeviceType") + } + if len(config.Name) >= 15 { + return nil, errors.New("device name is too long") + } + + if fd, err = syscall.Open( + "/dev/"+config.Name, os.O_RDWR|syscall.O_NONBLOCK, 0); err != nil { + return nil, err + } + // Note that we are not setting NONBLOCK on the fd itself since it breaks tuntaposx + // see https://sourceforge.net/p/tuntaposx/bugs/6/ + + // create socket so we can do SIO ioctls, we are not using it afterwards + if socketFD, err = syscall.Socket(syscall.AF_SYSTEM, syscall.SOCK_DGRAM, 2); err != nil { + return nil, fmt.Errorf("error in syscall.Socket: %v", err) + } + var ifReq = &struct { + ifName [16]byte + ifruFlags int16 + pad [16]byte + }{} + copy(ifReq.ifName[:], []byte(config.Name)) + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(socketFD), uintptr(syscall.SIOCGIFFLAGS), uintptr(unsafe.Pointer(ifReq))); errno != 0 { + err = errno + return nil, fmt.Errorf("error in syscall.Syscall(syscall.SYS_IOCTL, ...): %v", err) + } + ifReq.ifruFlags |= syscall.IFF_RUNNING | syscall.IFF_UP + if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(socketFD), uintptr(syscall.SIOCSIFFLAGS), uintptr(unsafe.Pointer(ifReq))); errno != 0 { + err = errno + return nil, fmt.Errorf("error in syscall.Syscall(syscall.SYS_IOCTL, ...): %v", err) + } + syscall.Close(socketFD) + + return &Interface{ + isTAP: config.DeviceType == TAP, + ReadWriteCloser: os.NewFile(uintptr(fd), "tun"), + name: config.Name, + }, nil +} + +// tunReadCloser is a hack to work around the first 4 bytes "packet +// information" because there doesn't seem to be an IFF_NO_PI for darwin. +type tunReadCloser struct { + f io.ReadWriteCloser + + rMu sync.Mutex + rBuf []byte + + wMu sync.Mutex + wBuf []byte +} + +var _ io.ReadWriteCloser = (*tunReadCloser)(nil) + +func (t *tunReadCloser) Read(to []byte) (int, error) { + t.rMu.Lock() + defer t.rMu.Unlock() + + if cap(t.rBuf) < len(to)+4 { + t.rBuf = make([]byte, len(to)+4) + } + t.rBuf = t.rBuf[:len(to)+4] + + n, err := t.f.Read(t.rBuf) + copy(to, t.rBuf[4:]) + return n - 4, err +} + +func (t *tunReadCloser) Write(from []byte) (int, error) { + + if len(from) == 0 { + return 0, syscall.EIO + } + + t.wMu.Lock() + defer t.wMu.Unlock() + + if cap(t.wBuf) < len(from)+4 { + t.wBuf = make([]byte, len(from)+4) + } + t.wBuf = t.wBuf[:len(from)+4] + + // Determine the IP Family for the NULL L2 Header + ipVer := from[0] >> 4 + if ipVer == 4 { + t.wBuf[3] = syscall.AF_INET + } else if ipVer == 6 { + t.wBuf[3] = syscall.AF_INET6 + } else { + return 0, errors.New("Unable to determine IP version from packet") + } + + copy(t.wBuf[4:], from) + + n, err := t.f.Write(t.wBuf) + return n - 4, err +} + +func (t *tunReadCloser) Close() error { + return t.f.Close() +} diff --git a/syscalls_darwin_go1.11.go b/syscalls_darwin_go1.11.go new file mode 100644 index 0000000..b02f734 --- /dev/null +++ b/syscalls_darwin_go1.11.go @@ -0,0 +1,9 @@ +// +build darwin,go1.11 + +package water + +import "syscall" + +func setNonBlock(fd int) error { + return syscall.SetNonblock(fd, true) +} diff --git a/syscalls_darwin_legacy.go b/syscalls_darwin_legacy.go new file mode 100644 index 0000000..d174540 --- /dev/null +++ b/syscalls_darwin_legacy.go @@ -0,0 +1,10 @@ +// +build darwin,!go1.11 + +package water + +func setNonBlock(fd int) error { + // There's a but pre-go1.11 that causes 'resource temporarily unavailable' + // error in non-blocking mode. So just skip it here. Close() won't be able + // to unblock a pending read, but that's better than being broken. + return nil +} diff --git a/syscalls_linux.go b/syscalls_linux.go new file mode 100644 index 0000000..af382d1 --- /dev/null +++ b/syscalls_linux.go @@ -0,0 +1,83 @@ +package water + +import ( + "os" + "strings" + "syscall" + "unsafe" +) + +const ( + cIFFTUN = 0x0001 + cIFFTAP = 0x0002 + cIFFNOPI = 0x1000 + cIFFMULTIQUEUE = 0x0100 +) + +type ifReq struct { + Name [0x10]byte + Flags uint16 + pad [0x28 - 0x10 - 2]byte +} + +func ioctl(fd uintptr, request uintptr, argp uintptr) error { + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(request), argp) + if errno != 0 { + return os.NewSyscallError("ioctl", errno) + } + return nil +} + +func setupFd(config Config, fd uintptr) (name string, err error) { + var flags uint16 = cIFFNOPI + if config.DeviceType == TUN { + flags |= cIFFTUN + } else { + flags |= cIFFTAP + } + if config.PlatformSpecificParams.MultiQueue { + flags |= cIFFMULTIQUEUE + } + + if name, err = createInterface(fd, config.Name, flags); err != nil { + return "", err + } + + if err = setDeviceOptions(fd, config); err != nil { + return "", err + } + + return name, nil +} + +func createInterface(fd uintptr, ifName string, flags uint16) (createdIFName string, err error) { + var req ifReq + req.Flags = flags + copy(req.Name[:], ifName) + + err = ioctl(fd, syscall.TUNSETIFF, uintptr(unsafe.Pointer(&req))) + if err != nil { + return + } + + createdIFName = strings.Trim(string(req.Name[:]), "\x00") + return +} + +func setDeviceOptions(fd uintptr, config Config) (err error) { + if config.Permissions != nil { + if err = ioctl(fd, syscall.TUNSETOWNER, uintptr(config.Permissions.Owner)); err != nil { + return + } + if err = ioctl(fd, syscall.TUNSETGROUP, uintptr(config.Permissions.Group)); err != nil { + return + } + } + + // set clear the persist flag + value := 0 + if config.Persist { + value = 1 + } + return ioctl(fd, syscall.TUNSETPERSIST, uintptr(value)) +} diff --git a/syscalls_linux_go1.11.go b/syscalls_linux_go1.11.go new file mode 100644 index 0000000..90c47a1 --- /dev/null +++ b/syscalls_linux_go1.11.go @@ -0,0 +1,27 @@ +// +build linux,go1.11 + +package water + +import ( + "os" + "syscall" +) + +func openDev(config Config) (ifce *Interface, err error) { + var fdInt int + if fdInt, err = syscall.Open( + "/dev/net/tun", os.O_RDWR|syscall.O_NONBLOCK, 0); err != nil { + return nil, err + } + + name, err := setupFd(config, uintptr(fdInt)) + if err != nil { + return nil, err + } + + return &Interface{ + isTAP: config.DeviceType == TAP, + ReadWriteCloser: os.NewFile(uintptr(fdInt), "tun"), + name: name, + }, nil +} diff --git a/syscalls_linux_legacy.go b/syscalls_linux_legacy.go new file mode 100644 index 0000000..6499b37 --- /dev/null +++ b/syscalls_linux_legacy.go @@ -0,0 +1,26 @@ +// +build linux,!go1.11 + +package water + +import ( + "os" +) + +func openDev(config Config) (ifce *Interface, err error) { + var file *os.File + if file, err = os.OpenFile( + "/dev/net/tun", os.O_RDWR, 0); err != nil { + return nil, err + } + + name, err := setupFd(config, file.Fd()) + if err != nil { + return nil, err + } + + return &Interface{ + isTAP: config.DeviceType == TAP, + ReadWriteCloser: file, + name: name, + }, nil +} diff --git a/syscalls_other.go b/syscalls_other.go new file mode 100644 index 0000000..c5ed1b3 --- /dev/null +++ b/syscalls_other.go @@ -0,0 +1,9 @@ +// +build !linux,!darwin,!windows + +package water + +import "errors" + +func openDev(config Config) (*Interface, error) { + return nil, errors.New("not implemented on this platform") +} diff --git a/syscalls_windows.go b/syscalls_windows.go new file mode 100644 index 0000000..4ae5280 --- /dev/null +++ b/syscalls_windows.go @@ -0,0 +1,41 @@ +package water + +import ( + "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/tun" +) + +type wintun struct { + dev tun.Device +} + +func (w *wintun) Close() error { + return w.dev.Close() +} + +func (w *wintun) Write(b []byte) (int, error) { + return w.dev.Write(b, 0) +} + +func (w *wintun) Read(b []byte) (int, error) { + return w.dev.Read(b, 0) +} + +func openDev(config Config) (ifce *Interface, err error) { + if config.DeviceType == TAP { + return nil, err + } + id := &windows.GUID{ + 0x0000000, + 0xFFFF, + 0xFFFF, + [8]byte{0xFF, 0xe9, 0x76, 0xe5, 0x8c, 0x74, 0x06, 0x3e}, + } + dev, err := tun.CreateTUNWithRequestedGUID(config.PlatformSpecificParams.InterfaceName, id, 0) + if err != nil { + return nil, err + } + wintun := &wintun{dev: dev} + ifce = &Interface{isTAP: (config.DeviceType == TAP), ReadWriteCloser: wintun} + return ifce, nil +}