-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[vnet][1] setup TUN and IPv6 on MacOS #40893
Changes from all commits
d37af86
f4cfdeb
fa6c9cf
7422a35
b82edbf
01ed3f4
d79309a
864ee56
dffd4d3
c1de859
a0e773d
48a0bec
ee5f5c8
ebef36a
fc0e77f
36f48ec
1b88e36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
nklaassen marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
To make it more clear, at the moment all you can do is try to reach the IPv6 address directly, for example:
So I grab
Maybe there's an easier way to do this, but running that is what worked for me to trigger the following output from
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be even more clear, even this no longer works, since I removed promiscuous mode in the previous PR this no longer handles packets destined for an IP address that hasn't been assigned yet, and outside of tests, no IPs are assigned yet. Things start actually working in #41031 where we assign an IP for DNS, and querying DNS for an app name assigns an IP to that app |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
// Teleport | ||
// Copyright (C) 2024 Gravitational, Inc. | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Affero General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Affero General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Affero General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
package vnet | ||
|
||
import ( | ||
"context" | ||
"log/slog" | ||
"os" | ||
|
||
"github.com/gravitational/trace" | ||
"golang.zx2c4.com/wireguard/tun" | ||
) | ||
|
||
// Run is a blocking call to create and start Teleport VNet. | ||
func Run(ctx context.Context) error { | ||
ibeckermayer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ipv6Prefix, err := IPv6Prefix() | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
tun, err := CreateAndSetupTUNDevice(ctx, ipv6Prefix.String()) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
manager, err := NewManager(&Config{ | ||
TUNDevice: tun, | ||
IPv6Prefix: ipv6Prefix, | ||
}) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
return trace.Wrap(manager.Run(ctx)) | ||
} | ||
|
||
// AdminSubcommand is the tsh subcommand that should run as root that will | ||
// create and setup a TUN device and pass the file descriptor for that device | ||
// over the unix socket found at socketPath. | ||
func AdminSubcommand(ctx context.Context, socketPath, ipv6Prefix string) error { | ||
tun, tunName, err := createAndSetupTUNDeviceAsRoot(ctx, ipv6Prefix) | ||
if err != nil { | ||
return trace.Wrap(err, "performing admin setup") | ||
} | ||
if err := sendTUNNameAndFd(socketPath, tunName, tun.File().Fd()); err != nil { | ||
return trace.Wrap(err) | ||
} | ||
return nil | ||
} | ||
|
||
// CreateAndSetupTUNDevice returns a virtual network device and configures the host OS to use that device for | ||
// VNet connections. | ||
func CreateAndSetupTUNDevice(ctx context.Context, ipv6Prefix string) (tun.Device, error) { | ||
var ( | ||
device tun.Device | ||
name string | ||
err error | ||
) | ||
if os.Getuid() == 0 { | ||
// We can get here if the user runs `tsh vnet` as root, but it is not in the expected path when | ||
// started as a regular user. Typically we expect `tsh vnet` to be run as a non-root user, and for | ||
// AdminSubcommand to directly call createAndSetupTUNDeviceAsRoot. | ||
device, name, err = createAndSetupTUNDeviceAsRoot(ctx, ipv6Prefix) | ||
} else { | ||
device, name, err = createAndSetupTUNDeviceWithoutRoot(ctx, ipv6Prefix) | ||
} | ||
if err != nil { | ||
return nil, trace.Wrap(err) | ||
} | ||
slog.InfoContext(ctx, "Created TUN device.", "device", name) | ||
return device, nil | ||
} | ||
|
||
func createAndSetupTUNDeviceAsRoot(ctx context.Context, ipv6Prefix string) (tun.Device, string, error) { | ||
ibeckermayer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
tun, tunName, err := createTUNDevice(ctx) | ||
if err != nil { | ||
return nil, "", trace.Wrap(err) | ||
} | ||
|
||
tunIPv6 := ipv6Prefix + "1" | ||
nklaassen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cfg := osConfig{ | ||
tunName: tunName, | ||
tunIPv6: tunIPv6, | ||
} | ||
if err := configureOS(ctx, &cfg); err != nil { | ||
return nil, "", trace.Wrap(err, "configuring OS") | ||
} | ||
|
||
return tun, tunName, nil | ||
} | ||
|
||
func createTUNDevice(ctx context.Context) (tun.Device, string, error) { | ||
slog.DebugContext(ctx, "Creating TUN device.") | ||
dev, err := tun.CreateTUN("utun", mtu) | ||
if err != nil { | ||
return nil, "", trace.Wrap(err, "creating TUN device") | ||
} | ||
name, err := dev.Name() | ||
if err != nil { | ||
return nil, "", trace.Wrap(err, "getting TUN device name") | ||
} | ||
return dev, name, nil | ||
} | ||
|
||
type osConfig struct { | ||
tunName string | ||
tunIPv6 string | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
// Teleport | ||
// Copyright (C) 2024 Gravitational, Inc. | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU Affero General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU Affero General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU Affero General Public License | ||
// along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
//go:build darwin | ||
// +build darwin | ||
|
||
package vnet | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"log/slog" | ||
"net" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/google/uuid" | ||
"github.com/gravitational/trace" | ||
"golang.org/x/sys/unix" | ||
"golang.zx2c4.com/wireguard/tun" | ||
|
||
"github.com/gravitational/teleport" | ||
) | ||
|
||
const ( | ||
tunHandoverTimeout = time.Minute | ||
) | ||
|
||
func createAndSetupTUNDeviceWithoutRoot(ctx context.Context, ipv6Prefix string) (tun.Device, string, error) { | ||
slog.InfoContext(ctx, "Spawning child process as root to create and setup TUN device") | ||
socket, socketPath, err := createUnixSocket() | ||
if err != nil { | ||
return nil, "", trace.Wrap(err) | ||
} | ||
|
||
ctx, cancel := context.WithCancel(ctx) | ||
defer cancel() | ||
|
||
adminCommandErr := make(chan error, 1) | ||
go func() { | ||
adminCommandErr <- trace.Wrap(execAdminSubcommand(ctx, socketPath, ipv6Prefix)) | ||
}() | ||
|
||
recvTunErr := make(chan error, 1) | ||
var tunName string | ||
var tunFd uintptr | ||
go func() { | ||
tunName, tunFd, err = recvTUNNameAndFd(ctx, socket) | ||
recvTunErr <- trace.Wrap(err, "receiving TUN name and file descriptor") | ||
}() | ||
|
||
loop: | ||
for { | ||
select { | ||
case err := <-adminCommandErr: | ||
if err != nil { | ||
return nil, "", trace.Wrap(err) | ||
} | ||
case err := <-recvTunErr: | ||
if err != nil { | ||
return nil, "", trace.Wrap(err) | ||
} | ||
break loop | ||
} | ||
} | ||
|
||
tunDevice, err := tun.CreateTUNFromFile(os.NewFile(tunFd, ""), 0) | ||
if err != nil { | ||
return nil, "", trace.Wrap(err, "creating TUN device from file descriptor") | ||
} | ||
|
||
return tunDevice, tunName, nil | ||
} | ||
|
||
func execAdminSubcommand(ctx context.Context, socketPath, ipv6Prefix string) error { | ||
executableName, err := os.Executable() | ||
if err != nil { | ||
return trace.Wrap(err, "getting executable path") | ||
} | ||
|
||
appleScript := fmt.Sprintf(` | ||
set executableName to "%s" | ||
set socketPath to "%s" | ||
set ipv6Prefix to "%s" | ||
do shell script quoted form of executableName & `+ | ||
`" %s --socket " & quoted form of socketPath & `+ | ||
`" --ipv6-prefix " & quoted form of ipv6Prefix `+ | ||
`with prompt "VNet wants to set up a virtual network device" with administrator privileges`, | ||
executableName, socketPath, ipv6Prefix, teleport.VnetAdminSetupSubCommand) | ||
|
||
// The context we pass here has effect only on the password prompt being shown. Once osascript spawns the | ||
// privileged process, canceling the context (and thus killing osascript) has no effect on the privileged | ||
// process. | ||
cmd := exec.CommandContext(ctx, "osascript", "-e", appleScript) | ||
var stderr strings.Builder | ||
cmd.Stderr = &stderr | ||
|
||
if err := cmd.Run(); err != nil { | ||
var exitError *exec.ExitError | ||
if errors.As(err, &exitError) { | ||
stderr := stderr.String() | ||
|
||
// When the user closes the prompt for administrator privileges, the -128 error is returned. | ||
// https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_codes.html#//apple_ref/doc/uid/TP40000983-CH220-SW2 | ||
if strings.Contains(stderr, "-128") { | ||
return trace.Errorf("password prompt closed by user") | ||
} | ||
return trace.Wrap(exitError, "admin subcommand exited, stderr: %s", stderr) | ||
} | ||
return trace.Wrap(err) | ||
} | ||
return nil | ||
} | ||
|
||
func createUnixSocket() (*net.UnixListener, string, error) { | ||
socketPath := filepath.Join(os.TempDir(), "vnet"+uuid.NewString()+".sock") | ||
socketAddr := &net.UnixAddr{Name: socketPath, Net: "unix"} | ||
l, err := net.ListenUnix(socketAddr.Net, socketAddr) | ||
if err != nil { | ||
return nil, "", trace.Wrap(err, "creating unix socket") | ||
} | ||
if err := os.Chmod(socketPath, 0o600); err != nil { | ||
return nil, "", trace.Wrap(err, "setting permissions on unix socket") | ||
} | ||
return l, socketPath, nil | ||
} | ||
|
||
// sendTUNNameAndFd sends the name of the TUN device and its open file descriptor over a unix socket, meant | ||
// for passing the TUN from the root process which must create it to the user process. | ||
func sendTUNNameAndFd(socketPath, tunName string, fd uintptr) error { | ||
socketAddr := &net.UnixAddr{Name: socketPath, Net: "unix"} | ||
conn, err := net.DialUnix(socketAddr.Net, nil /*laddr*/, socketAddr) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
defer conn.Close() | ||
|
||
err = conn.SetDeadline(time.Now().Add(tunHandoverTimeout)) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
// Write the device name as the main message and pass the file desciptor as out-of-band data. | ||
rights := unix.UnixRights(int(fd)) | ||
if _, _, err := conn.WriteMsgUnix([]byte(tunName), rights, socketAddr); err != nil { | ||
return trace.Wrap(err, "writing to unix conn") | ||
} | ||
return nil | ||
} | ||
|
||
// recvTUNNameAndFd receives the name of a TUN device and its open file descriptor over a unix socket, meant | ||
// for passing the TUN from the root process which must create it to the user process. | ||
func recvTUNNameAndFd(ctx context.Context, socket *net.UnixListener) (string, uintptr, error) { | ||
ctx, cancel := context.WithTimeout(ctx, tunHandoverTimeout) | ||
defer cancel() | ||
deadline, _ := ctx.Deadline() | ||
|
||
err := socket.SetDeadline(deadline) | ||
if err != nil { | ||
return "", 0, trace.Wrap(err) | ||
} | ||
go func() { | ||
<-ctx.Done() | ||
socket.Close() | ||
}() | ||
|
||
conn, err := socket.AcceptUnix() | ||
if err != nil { | ||
return "", 0, trace.Wrap(err) | ||
} | ||
go func() { | ||
// Close the connection early to unblock reads if the context is canceled. | ||
<-ctx.Done() | ||
conn.Close() | ||
}() | ||
|
||
msg := make([]byte, 128) | ||
oob := make([]byte, unix.CmsgSpace(4)) // Fd is 4 bytes | ||
n, oobn, _, _, err := conn.ReadMsgUnix(msg, oob) | ||
if err != nil { | ||
return "", 0, trace.Wrap(err, "reading from unix conn") | ||
} | ||
|
||
// Parse the device name from the main message. | ||
if n == 0 { | ||
return "", 0, trace.Errorf("failed to read msg from unix conn") | ||
} | ||
if oobn != len(oob) { | ||
return "", 0, trace.Errorf("failed to read out-of-band data from unix conn") | ||
} | ||
tunName := string(msg[:n]) | ||
|
||
// Parse the file descriptor from the out-of-band data. | ||
scm, err := unix.ParseSocketControlMessage(oob) | ||
if err != nil { | ||
return "", 0, trace.Wrap(err, "parsing socket control message") | ||
} | ||
if len(scm) != 1 { | ||
return "", 0, trace.BadParameter("expect 1 socket control message, got %d", len(scm)) | ||
} | ||
fds, err := unix.ParseUnixRights(&scm[0]) | ||
if err != nil { | ||
return "", 0, trace.Wrap(err, "parsing file descriptors") | ||
} | ||
if len(fds) != 1 { | ||
return "", 0, trace.BadParameter("expected 1 file descriptor, got %d", len(fds)) | ||
} | ||
fd := uintptr(fds[0]) | ||
|
||
return tunName, fd, nil | ||
} | ||
|
||
func configureOS(ctx context.Context, cfg *osConfig) error { | ||
ibeckermayer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if cfg.tunIPv6 != "" && cfg.tunName != "" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider inverting the condition here so that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to be adding more conditions later on - see https://github.com/gravitational/teleport/pull/41032/files#diff-7627839bca33fec5fb081fb7297edbdbaf62435ee4e8d8105c76ccbb7edf8150 |
||
slog.InfoContext(ctx, "Setting IPv6 address for the TUN device.", "device", cfg.tunName, "address", cfg.tunIPv6) | ||
cmd := exec.CommandContext(ctx, "ifconfig", cfg.tunName, "inet6", cfg.tunIPv6, "prefixlen", "64") | ||
if err := cmd.Run(); err != nil { | ||
return trace.Wrap(err, "running %v", cmd.Args) | ||
} | ||
|
||
slog.InfoContext(ctx, "Setting an IPv6 route for the VNet.") | ||
cmd = exec.CommandContext(ctx, "route", "add", "-inet6", cfg.tunIPv6, "-prefixlen", "64", "-interface", cfg.tunName) | ||
if err := cmd.Run(); err != nil { | ||
return trace.Wrap(err, "running %v", cmd.Args) | ||
} | ||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be consistent with
nklaassen/vnet0
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ravicious and I have decided to capitalize as Vnet in code - otherwise JS gRPC codegen starts creating things called
vNetXxxxx
. Logs and errors still useVNet
.