From a7290702526463e60e25ad126f569c858002a3fc Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 23 Jul 2021 09:45:04 -0700 Subject: [PATCH] net/tstun: add start of Linux TAP support, with DHCP+ARP server Still very much a prototype (hard-coded IPs, etc) but should be non-invasive enough to submit at this point and iterate from here. Updates #2589 Co-Author: David Crawshaw Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/depaware.txt | 13 +- cmd/tailscaled/tailscaled.go | 12 +- go.mod | 1 + go.sum | 14 ++ net/tstun/tap_linux.go | 359 +++++++++++++++++++++++++++++++++++ net/tstun/tap_unsupported.go | 10 + net/tstun/tun.go | 26 ++- net/tstun/wrap.go | 63 +++++- wgengine/userspace.go | 11 +- 9 files changed, 498 insertions(+), 11 deletions(-) create mode 100644 net/tstun/tap_linux.go create mode 100644 net/tstun/tap_unsupported.go diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 98547c100..53f1815c7 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -10,6 +10,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns github.com/golang/snappy from github.com/klauspost/compress/zstd github.com/google/btree from inet.af/netstack/tcpip/header+ + L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun + L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4 + L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4 + L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4 L github.com/josharian/native from github.com/mdlayher/netlink+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink @@ -30,6 +34,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+ github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck + L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4 + L github.com/u-root/uio/ubinary from github.com/u-root/uio/uio + L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+ 💣 go4.org/intern from inet.af/netaddr 💣 go4.org/mem from tailscale.com/derp+ go4.org/unsafe/assume-no-moving-gc from go4.org/intern @@ -66,7 +73,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+ inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+ inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+ - inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack + inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack+ inet.af/netstack/tcpip/network/ipv6 from tailscale.com/wgengine/netstack inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+ inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+ @@ -121,7 +128,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+ - tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+ + 💣 tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+ tailscale.com/paths from tailscale.com/cmd/tailscaled+ tailscale.com/portlist from tailscale.com/ipn/ipnlocal tailscale.com/safesocket from tailscale.com/ipn/ipnserver+ @@ -140,7 +147,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/types/netmap from tailscale.com/control/controlclient+ tailscale.com/types/nettype from tailscale.com/wgengine/magicsock tailscale.com/types/opt from tailscale.com/control/controlclient+ - tailscale.com/types/pad32 from tailscale.com/derp + tailscale.com/types/pad32 from tailscale.com/net/tstun+ tailscale.com/types/persist from tailscale.com/control/controlclient+ tailscale.com/types/preftype from tailscale.com/ipn+ tailscale.com/types/structs from tailscale.com/control/controlclient+ diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 675650adc..005a64439 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -68,9 +68,13 @@ func defaultTunName() string { } var args struct { + // tunname is a /dev/net/tun tunnel name ("tailscale0"), the + // string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]" + // or comma-separated list thereof. + tunname string + cleanup bool debug string - tunname string // tun name, "userspace-networking", or comma-separated list thereof port uint16 statepath string socketpath string @@ -352,6 +356,12 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine. return nil, false, err } conf.Tun = dev + if strings.HasPrefix(name, "tap:") { + conf.IsTAP = true + e, err := wgengine.NewUserspaceEngine(logf, conf) + return e, false, err + } + r, err := router.New(logf, dev, linkMon) if err != nil { dev.Close() diff --git a/go.mod b/go.mod index ab84f5245..cdcd59e4e 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/google/uuid v1.1.2 github.com/goreleaser/nfpm v1.10.3 github.com/iancoleman/strcase v0.2.0 + github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.12.2 diff --git a/go.sum b/go.sum index 3a8f27dc8..add86197c 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,7 @@ github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= @@ -297,6 +298,7 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -304,6 +306,8 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e h1:sgh63o+pm5kcdrgyYaCIoeD7mccyL6MscVmy+DvY6C4= +github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -326,6 +330,7 @@ github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/rasw github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= +github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= @@ -391,6 +396,7 @@ github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpe github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mbilski/exhaustivestruct v1.1.0 h1:4ykwscnAFeHJruT+EY3M3vdeP8uXMh0VV2E61iR7XD8= github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY= github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0= @@ -406,6 +412,8 @@ github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuri github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8= github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI= github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q= +github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697 h1:PBb7ld5cQGfxHF2pKvb/ydtuPwdRaltGI4e0QSCuiNI= github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4= github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 h1:qEtkL8n1DAHpi5/AOgAckwGQUlMe4+jhL/GMt+GKIks= @@ -604,6 +612,8 @@ github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa h1:RC4maTWLK github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig= github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4= github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA= @@ -699,6 +709,7 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -755,9 +766,11 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -777,6 +790,7 @@ golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/net/tstun/tap_linux.go b/net/tstun/tap_linux.go new file mode 100644 index 000000000..0e6ef7258 --- /dev/null +++ b/net/tstun/tap_linux.go @@ -0,0 +1,359 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tstun + +import ( + "fmt" + "net" + "os" + "os/exec" + "syscall" + "unsafe" + + "github.com/insomniacslk/dhcp/dhcpv4" + "golang.zx2c4.com/wireguard/tun" + "inet.af/netaddr" + "inet.af/netstack/tcpip" + "inet.af/netstack/tcpip/buffer" + "inet.af/netstack/tcpip/header" + "inet.af/netstack/tcpip/network/ipv4" + "inet.af/netstack/tcpip/transport/udp" + "tailscale.com/net/packet" + "tailscale.com/types/ipproto" +) + +// TODO: this was randomly generated once. Maybe do it per process start? But +// then an upgraded tailscaled would be visible to devices behind it. So +// maybe instead make it a function of the tailscaled's wireguard public key? +// For now just hard code it. +var ourMAC = net.HardwareAddr{0x30, 0x2D, 0x66, 0xEC, 0x7A, 0x93} + +func init() { createTAP = createTAPLinux } + +func createTAPLinux(tapName, bridgeName string) (dev tun.Device, err error) { + fd, err := syscall.Open("/dev/net/tun", syscall.O_RDWR, 0) + if err != nil { + return nil, err + } + var ifr struct { + name [16]byte + flags uint16 + _ [22]byte + } + copy(ifr.name[:], tapName) + ifr.flags = syscall.IFF_TAP | syscall.IFF_NO_PI + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ifr))) + if errno != 0 { + syscall.Close(fd) + return nil, errno + } + if err = syscall.SetNonblock(fd, true); err != nil { + syscall.Close(fd) + return nil, err + } + + if err := run("ip", "link", "set", "dev", tapName, "up"); err != nil { + return nil, err + } + if bridgeName != "" { + if err := run("brctl", "addif", bridgeName, tapName); err != nil { + return nil, err + } + } + dev, _, err = tun.CreateUnmonitoredTUNFromFD(fd) // TODO: MTU + if err != nil { + syscall.Close(fd) + return nil, err + } + return dev, nil +} + +type etherType [2]byte + +var ( + etherTypeARP = etherType{0x08, 0x06} + etherTypeIPv4 = etherType{0x08, 0x00} + etherTypeIPv6 = etherType{0x86, 0xDD} +) + +const ipv4HeaderLen = 20 + +const ( + consumePacket = true + passOnPacket = false +) + +// handleTAPFrame handles receiving a raw TAP ethernet frame and reports whether +// it's been handled (that is, whether it should NOT be passed to wireguard). +func (t *Wrapper) handleTAPFrame(ethBuf []byte) bool { + + if len(ethBuf) < ethernetFrameSize { + // Corrupt. Ignore. + if tapDebug { + t.logf("tap: short TAP frame") + } + return consumePacket + } + ethDstMAC, ethSrcMAC := ethBuf[:6], ethBuf[6:12] + _ = ethDstMAC + et := etherType{ethBuf[12], ethBuf[13]} + switch et { + default: + if tapDebug { + t.logf("tap: ignoring etherType %v", et) + } + return consumePacket // filter out packet we should ignore + case etherTypeIPv6: + // TODO: support DHCPv6/ND/etc later. For now pass all to WireGuard. + if tapDebug { + t.logf("tap: ignoring IPv6 %v", et) + } + return passOnPacket + case etherTypeIPv4: + if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen { + // Bogus IPv4. Eat. + if tapDebug { + t.logf("tap: short ipv4") + } + return consumePacket + } + return t.handleDHCPRequest(ethBuf) + case etherTypeARP: + arpPacket := header.ARP(ethBuf[ethernetFrameSize:]) + if !arpPacket.IsValid() { + // Bogus ARP. Eat. + return consumePacket + } + switch arpPacket.Op() { + case header.ARPRequest: + req := arpPacket // better name at this point + buf := make([]byte, header.EthernetMinimumSize+header.ARPSize) + + // Our ARP "Table" of one: + var srcMAC [6]byte + copy(srcMAC[:], ethSrcMAC) + if old := t.destMAC(); old != srcMAC { + t.destMACAtomic.Store(srcMAC) + } + + eth := header.Ethernet(buf) + eth.Encode(&header.EthernetFields{ + SrcAddr: tcpip.LinkAddress(ourMAC[:]), + DstAddr: tcpip.LinkAddress(ethSrcMAC), + Type: 0x0806, // arp + }) + res := header.ARP(buf[header.EthernetMinimumSize:]) + res.SetIPv4OverEthernet() + res.SetOp(header.ARPReply) + + // If the client's asking about their own IP, tell them it's + // their own MAC. TODO(bradfitz): remove String allocs. + if net.IP(req.ProtocolAddressTarget()).String() == theClientIP { + copy(res.HardwareAddressSender(), ethSrcMAC) + } else { + copy(res.HardwareAddressSender(), ourMAC[:]) + } + + copy(res.ProtocolAddressSender(), req.ProtocolAddressTarget()) + copy(res.HardwareAddressTarget(), req.HardwareAddressSender()) + copy(res.ProtocolAddressTarget(), req.ProtocolAddressSender()) + + n, err := t.tdev.Write(buf, 0) + if tapDebug { + t.logf("tap: wrote ARP reply %v, %v", n, err) + } + } + + return consumePacket + } +} + +// TODO(bradfitz): remove these hard-coded values and move from a /24 to a /10 CGNAT as the range. +const theClientIP = "100.70.145.3" // TODO: make dynamic from netmap +const routerIP = "100.70.145.1" // must be in same netmask (currently hack at /24) as theClientIP + +// handleDHCPRequest handles receiving a raw TAP ethernet frame and reports whether +// it's been handled as a DHCP request. That is, it reports whether the frame should +// be ignored by the caller and not passed on. +func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool { + const udpHeader = 8 + if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen+udpHeader { + if tapDebug { + t.logf("tap: DHCP short") + } + return passOnPacket + } + ethDstMAC, ethSrcMAC := ethBuf[:6], ethBuf[6:12] + + if string(ethDstMAC) != "\xff\xff\xff\xff\xff\xff" { + // Not a broadcast + if tapDebug { + t.logf("tap: dhcp no broadcast") + } + return passOnPacket + } + + p := parsedPacketPool.Get().(*packet.Parsed) + defer parsedPacketPool.Put(p) + p.Decode(ethBuf[ethernetFrameSize:]) + + if p.IPProto != ipproto.UDP || p.Src.Port() != 68 || p.Dst.Port() != 67 { + // Not a DHCP request. + if tapDebug { + t.logf("tap: DHCP wrong meta") + } + return passOnPacket + } + + dp, err := dhcpv4.FromBytes(ethBuf[ethernetFrameSize+ipv4HeaderLen+udpHeader:]) + if err != nil { + // Bogus. Trash it. + if tapDebug { + t.logf("tap: DHCP FromBytes bad") + } + return consumePacket + } + if tapDebug { + t.logf("tap: DHCP request: %+v", dp) + } + switch dp.MessageType() { + case dhcpv4.MessageTypeDiscover: + offer, err := dhcpv4.New( + dhcpv4.WithReply(dp), + dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer), + dhcpv4.WithRouter(net.ParseIP(routerIP)), // the default route + dhcpv4.WithDNS(net.ParseIP("100.100.100.100")), + dhcpv4.WithServerIP(net.ParseIP("100.100.100.100")), // TODO: what is this? + dhcpv4.WithOption(dhcpv4.OptServerIdentifier(net.ParseIP("100.100.100.100"))), + dhcpv4.WithYourIP(net.ParseIP(theClientIP)), + dhcpv4.WithLeaseTime(3600), // hour works + //dhcpv4.WithHwAddr(ethSrcMAC), + dhcpv4.WithNetmask(net.IPMask(net.ParseIP("255.255.255.0").To4())), // TODO: wrong + //dhcpv4.WithTransactionID(dp.TransactionID), + ) + if err != nil { + t.logf("error building DHCP offer: %v", err) + return consumePacket + } + // Make a layer 2 packet to write out: + pkt := packLayer2UDP( + offer.ToBytes(), + ourMAC, ethSrcMAC, + netaddr.IPPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src + netaddr.IPPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst + ) + n, err := t.tdev.Write(pkt, 0) + if tapDebug { + t.logf("tap: wrote DHCP OFFER %v, %v", n, err) + } + case dhcpv4.MessageTypeRequest: + ack, err := dhcpv4.New( + dhcpv4.WithReply(dp), + dhcpv4.WithMessageType(dhcpv4.MessageTypeAck), + dhcpv4.WithDNS(net.ParseIP("100.100.100.100")), + dhcpv4.WithRouter(net.ParseIP(routerIP)), // the default route + dhcpv4.WithServerIP(net.ParseIP("100.100.100.100")), // TODO: what is this? + dhcpv4.WithOption(dhcpv4.OptServerIdentifier(net.ParseIP("100.100.100.100"))), + dhcpv4.WithYourIP(net.ParseIP(theClientIP)), // Hello world + dhcpv4.WithLeaseTime(3600), // hour works + dhcpv4.WithNetmask(net.IPMask(net.ParseIP("255.255.255.0").To4())), + ) + if err != nil { + t.logf("error building DHCP ack: %v", err) + return consumePacket + } + // Make a layer 2 packet to write out: + pkt := packLayer2UDP( + ack.ToBytes(), + ourMAC, ethSrcMAC, + netaddr.IPPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src + netaddr.IPPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst + ) + n, err := t.tdev.Write(pkt, 0) + if tapDebug { + t.logf("tap: wrote DHCP ACK %v, %v", n, err) + } + default: + if tapDebug { + t.logf("tap: unknown DHCP type") + } + } + return consumePacket +} + +func packLayer2UDP(payload []byte, srcMAC, dstMAC net.HardwareAddr, src, dst netaddr.IPPort) []byte { + buf := buffer.NewView(header.EthernetMinimumSize + header.UDPMinimumSize + header.IPv4MinimumSize + len(payload)) + payloadStart := len(buf) - len(payload) + copy(buf[payloadStart:], payload) + srcB := src.IP().As4() + srcIP := tcpip.Address(srcB[:]) + dstB := dst.IP().As4() + dstIP := tcpip.Address(dstB[:]) + // Ethernet header + eth := header.Ethernet(buf) + eth.Encode(&header.EthernetFields{ + SrcAddr: tcpip.LinkAddress(srcMAC), + DstAddr: tcpip.LinkAddress(dstMAC), + Type: ipv4.ProtocolNumber, + }) + // IP header + ipbuf := buf[header.EthernetMinimumSize:] + ip := header.IPv4(ipbuf) + ip.Encode(&header.IPv4Fields{ + TotalLength: uint16(len(ipbuf)), + TTL: 65, + Protocol: uint8(udp.ProtocolNumber), + SrcAddr: srcIP, + DstAddr: dstIP, + }) + ip.SetChecksum(^ip.CalculateChecksum()) + // UDP header + u := header.UDP(buf[header.EthernetMinimumSize+header.IPv4MinimumSize:]) + u.Encode(&header.UDPFields{ + SrcPort: src.Port(), + DstPort: dst.Port(), + Length: uint16(header.UDPMinimumSize + len(payload)), + }) + // Calculate the UDP pseudo-header checksum. + xsum := header.PseudoHeaderChecksum(udp.ProtocolNumber, srcIP, dstIP, uint16(len(u))) + // Calculate the UDP checksum and set it. + xsum = header.Checksum(payload, xsum) + u.SetChecksum(^u.CalculateChecksum(xsum)) + return []byte(buf) +} + +func run(prog string, args ...string) error { + cmd := exec.Command(prog, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("error running %v: %v", cmd, err) + } + return nil +} + +func (t *Wrapper) destMAC() [6]byte { + mac, _ := t.destMACAtomic.Load().([6]byte) + return mac +} + +func (t *Wrapper) tapWrite(buf []byte, offset int) (int, error) { + if offset < ethernetFrameSize { + return 0, fmt.Errorf("[unexpected] weird offset %d for TAP write", offset) + } + eth := buf[offset-ethernetFrameSize:] + dst := t.destMAC() + copy(eth[:6], dst[:]) + copy(eth[6:12], ourMAC[:]) + et := etherTypeIPv4 + if buf[offset]>>4 == 6 { + et = etherTypeIPv6 + } + eth[12], eth[13] = et[0], et[1] + if tapDebug { + t.logf("tap: tapWrite off=%v % x", offset, buf) + } + return t.tdev.Write(buf, offset-ethernetFrameSize) +} diff --git a/net/tstun/tap_unsupported.go b/net/tstun/tap_unsupported.go new file mode 100644 index 000000000..af50ad9d7 --- /dev/null +++ b/net/tstun/tap_unsupported.go @@ -0,0 +1,10 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !linux + +package tstun + +func (*Wrapper) handleTAPFrame([]byte) bool { panic("unreachable") } +func (*Wrapper) tapWrite([]byte, int) (int, error) { panic("unreachable") } diff --git a/net/tstun/tun.go b/net/tstun/tun.go index 8bb1c6e28..7d8a52f08 100644 --- a/net/tstun/tun.go +++ b/net/tstun/tun.go @@ -8,10 +8,12 @@ package tstun import ( "bytes" + "errors" "os" "os/exec" "runtime" "strconv" + "strings" "time" "golang.zx2c4.com/wireguard/tun" @@ -35,10 +37,32 @@ func init() { } } +// createTAP is non-nil on Linux. +var createTAP func(tapName, bridgeName string) (tun.Device, error) + // New returns a tun.Device for the requested device name, along with // the OS-dependent name that was allocated to the device. func New(logf logger.Logf, tunName string) (tun.Device, string, error) { - dev, err := tun.CreateTUN(tunName, tunMTU) + var dev tun.Device + var err error + if strings.HasPrefix(tunName, "tap:") { + if runtime.GOOS != "linux" { + return nil, "", errors.New("tap only works on Linux") + } + f := strings.Split(tunName, ":") + var tapName, bridgeName string + switch len(f) { + case 2: + tapName = f[1] + case 3: + tapName, bridgeName = f[1], f[2] + default: + return nil, "", errors.New("bogus tap argument") + } + dev, err = createTAP(tapName, bridgeName) + } else { + dev, err = tun.CreateTUN(tunName, tunMTU) + } if err != nil { return nil, "", err } diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index 63d8b065a..e6e5eed74 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -8,8 +8,10 @@ package tstun import ( "errors" + "fmt" "io" "os" + "strings" "sync" "sync/atomic" "time" @@ -21,6 +23,7 @@ import ( "tailscale.com/tstime/mono" "tailscale.com/types/ipproto" "tailscale.com/types/logger" + "tailscale.com/types/pad32" "tailscale.com/wgengine/filter" ) @@ -35,6 +38,8 @@ const PacketStartOffset = device.MessageTransportHeaderSize // of a packet that can be injected into a tstun.Wrapper. const MaxPacketSize = device.MaxContentSize +const tapDebug = false // for super verbose TAP debugging + var ( // ErrClosed is returned when attempting an operation on a closed Wrapper. ErrClosed = errors.New("device closed") @@ -61,13 +66,16 @@ type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response type Wrapper struct { logf logger.Logf // tdev is the underlying Wrapper device. - tdev tun.Device + tdev tun.Device + isTAP bool // whether tdev is a TAP device closeOnce sync.Once + _ pad32.Four lastActivityAtomic mono.Time // time of last send or receive destIPActivity atomic.Value // of map[netaddr.IP]func() + destMACAtomic atomic.Value // of [6]byte // buffer stores the oldest unconsumed packet from tdev. // It is made a static buffer in order to avoid allocations. @@ -146,10 +154,19 @@ type tunReadResult struct { err error } +func WrapTAP(logf logger.Logf, tdev tun.Device) *Wrapper { + return wrap(logf, tdev, true) +} + func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper { + return wrap(logf, tdev, false) +} + +func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper { tun := &Wrapper{ - logf: logger.WithPrefix(logf, "tstun: "), - tdev: tdev, + logf: logger.WithPrefix(logf, "tstun: "), + isTAP: isTAP, + tdev: tdev, // bufferConsumed is conceptually a condition variable: // a goroutine should not block when setting it, even with no listeners. bufferConsumed: make(chan struct{}, 1), @@ -284,11 +301,14 @@ func allowSendOnClosedChannel() { panic(r) } +const ethernetFrameSize = 14 // 2 six byte MACs, 2 bytes ethertype + // poll polls t.tdev.Read, placing the oldest unconsumed packet into t.buffer. // This is needed because t.tdev.Read in general may block (it does on Windows), // so packets may be stuck in t.outbound if t.Read called t.tdev.Read directly. func (t *Wrapper) poll() { for range t.bufferConsumed { + DoRead: var n int var err error // Read may use memory in t.buffer before PacketStartOffset for mandatory headers. @@ -303,7 +323,33 @@ func (t *Wrapper) poll() { if t.isClosed() { return } - n, err = t.tdev.Read(t.buffer[:], PacketStartOffset) + if t.isTAP { + n, err = t.tdev.Read(t.buffer[:], PacketStartOffset-ethernetFrameSize) + if tapDebug { + s := fmt.Sprintf("% x", t.buffer[:]) + for strings.HasSuffix(s, " 00") { + s = strings.TrimSuffix(s, " 00") + } + t.logf("TAP read %v, %v: %s", n, err, s) + } + } else { + n, err = t.tdev.Read(t.buffer[:], PacketStartOffset) + } + } + if t.isTAP { + if err == nil { + ethernetFrame := t.buffer[PacketStartOffset-ethernetFrameSize:][:n] + if t.handleTAPFrame(ethernetFrame) { + goto DoRead + } + } + // Fall through. We got an IP packet. + if n >= ethernetFrameSize { + n -= ethernetFrameSize + } + if tapDebug { + t.logf("tap regular frame: %x", t.buffer[PacketStartOffset:PacketStartOffset+n]) + } } t.sendOutbound(tunReadResult{data: t.buffer[PacketStartOffset : PacketStartOffset+n], err: err}) } @@ -521,6 +567,13 @@ func (t *Wrapper) Write(buf []byte, offset int) (int, error) { } t.noteActivity() + return t.tdevWrite(buf, offset) +} + +func (t *Wrapper) tdevWrite(buf []byte, offset int) (int, error) { + if t.isTAP { + return t.tapWrite(buf, offset) + } return t.tdev.Write(buf, offset) } @@ -553,7 +606,7 @@ func (t *Wrapper) InjectInboundDirect(buf []byte, offset int) error { } // Write to the underlying device to skip filters. - _, err := t.tdev.Write(buf, offset) + _, err := t.tdevWrite(buf, offset) return err } diff --git a/wgengine/userspace.go b/wgengine/userspace.go index 8c65ae0f9..4a22c888a 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -149,6 +149,10 @@ type Config struct { // If nil, a fake Device that does nothing is used. Tun tun.Device + // IsTAP is whether Tun is actually a TAP (Layer 2) device that'll + // require ethernet headers. + IsTAP bool + // Router interfaces the Engine to the OS network stack. // If nil, a fake Router that does nothing is used. Router router.Router @@ -235,7 +239,12 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error) conf.DNS = d } - tsTUNDev := tstun.Wrap(logf, conf.Tun) + var tsTUNDev *tstun.Wrapper + if conf.IsTAP { + tsTUNDev = tstun.WrapTAP(logf, conf.Tun) + } else { + tsTUNDev = tstun.Wrap(logf, conf.Tun) + } closePool.add(tsTUNDev) e := &userspaceEngine{