mirror of
https://github.com/p1neappleXpress/OpenFlux.git
synced 2026-10-02 05:04:39 +08:00
0 off: status lines only
1 -d packet movement, one line per IPv4 packet:
-> 52 bytes - UDP 10.10.10.2:53000 -> 8.8.8.8:53 len=42 ttl=64
2 -dd plus operational logs (sessions, carriers, crypto, control)
3 -ddd plus hexdumps of packets and ciphertext
--debug=N sets the level directly; a bare --debug means -d. Only a run of
d's counts as the counted flag, so -direct-listen=... and other
single-dash long flags starting with d are no longer turned into
--debug. -d=N is accepted too. --sensetive is accepted as --sensitive,
as the previous commit message promised.
utils:
- Packetf logs at level 1, Debugf at level 2, IsVerbose() means level 3;
- the logger is created once and SetOutput swaps its writer, so the
mobile bridges can flip the level and the output while goroutines of
the previous connection are logging (from OpenFlux-Android 80fc99d);
- SetLogSink mirrors debug and packet lines to an embedding app, as the
Android bridge expects; EnableDebug/SetDebug(true) mean level 2, what
debug meant before levels;
- the unused Redact, Sensitivef and Verbosef are gone.
network.LogPacket is the one place that prints a packet line and, at
level 3, its hexdump. Every packet is logged once per side: the [NIC]
lines duplicated [TUNNEL] and are gone, and the iOS L3 path and
PacketTunnel, which logged nothing, now log too. On both sides "->"
points towards the internet and "<-" back towards the device: the L4
exit used to print them the other way round from L3 and the client, so
one flow now reads the same in both logs.
--sensitive now covers what can reveal a secret:
- key material, as before;
- hexdumps of plaintext frames (crypto plaintext, batch records, control
messages): control messages carry cookie jars, including an account
login, so -ddd alone never dumps them. Packet and ciphertext hexdumps
need only -ddd.
The [KEY] line and the [KEYDUMP] digest no longer print a SHA-256 of the
secret without --sensitive: it let anyone with the log test guesses
without paying for scrypt. The digest keeps the context and derived-key
prefixes, which is enough to compare peers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
406 lines
9.8 KiB
Go
406 lines
9.8 KiB
Go
//go:build darwin
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
"openflux/network"
|
|
"openflux/transport"
|
|
"openflux/utils"
|
|
)
|
|
|
|
// TUNClient is a macOS utun-based L3 forwarder: no gVisor, no SOCKS5.
|
|
// IP packets flow straight between the system utun interface and the transport.
|
|
type TUNClient struct {
|
|
trans transport.Transport
|
|
fd *os.File
|
|
name string
|
|
|
|
inbound chan []byte
|
|
|
|
packetsIn atomic.Uint64
|
|
packetsOut atomic.Uint64
|
|
|
|
gateway string
|
|
bypassIPs []string
|
|
routesAdded bool
|
|
|
|
savedIface string
|
|
savedGw string
|
|
defaultSet bool
|
|
}
|
|
|
|
func NewTUNClient(trans transport.Transport, mtu int) (*TUNClient, error) {
|
|
fd, name, err := openUtun()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open utun: %w", err)
|
|
}
|
|
|
|
c := &TUNClient{
|
|
trans: trans,
|
|
fd: fd,
|
|
name: name,
|
|
inbound: make(chan []byte, 4096),
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (c *TUNClient) Name() string { return c.name }
|
|
|
|
// ConfigureInterface sets address + routes. Requires root. Called once at start.
|
|
func (c *TUNClient) ConfigureInterface(bypassHosts []string) error {
|
|
setup := [][]string{
|
|
{"ifconfig", c.name, "10.10.10.2", "10.10.10.2", "up"},
|
|
{"ifconfig", c.name, "mtu", "1280"},
|
|
}
|
|
for _, args := range setup {
|
|
if out, err := exec.Command("sudo", args...).CombinedOutput(); err != nil {
|
|
return fmt.Errorf("%v: %w (%s)", args, err, string(out))
|
|
}
|
|
}
|
|
|
|
gw, iface, err := realGateway()
|
|
if err != nil {
|
|
return fmt.Errorf("find default gateway: %w", err)
|
|
}
|
|
c.gateway = gw
|
|
utils.Debugf("[TUN] real gateway: %s (iface=%s)", gw, iface)
|
|
|
|
for _, host := range bypassHosts {
|
|
host = strings.TrimSpace(host)
|
|
if host == "" {
|
|
continue
|
|
}
|
|
ips, err := net.LookupHost(host)
|
|
if err != nil {
|
|
utils.Debugf("[TUN] resolve %s failed: %v", host, err)
|
|
continue
|
|
}
|
|
for _, ip := range ips {
|
|
ipv4 := net.ParseIP(ip).To4()
|
|
if ipv4 == nil {
|
|
continue
|
|
}
|
|
ipStr := ipv4.String()
|
|
args := []string{"route", "add", "-host", ipStr, "-gateway", gw}
|
|
if out, err := exec.Command("sudo", args...).CombinedOutput(); err != nil {
|
|
utils.Debugf("[TUN] bypass route %s (%s) failed: %v (%s)", host, ipStr, err, string(out))
|
|
continue
|
|
}
|
|
c.bypassIPs = append(c.bypassIPs, ipStr)
|
|
utils.Debugf("[TUN] bypass %s -> %s via %s", host, ipStr, gw)
|
|
}
|
|
}
|
|
|
|
defaults := [][]string{
|
|
{"route", "add", "-net", "0.0.0.0/1", "-interface", c.name},
|
|
{"route", "add", "-net", "128.0.0.0/1", "-interface", c.name},
|
|
}
|
|
for _, args := range defaults {
|
|
if out, err := exec.Command("sudo", args...).CombinedOutput(); err != nil {
|
|
c.removeRoutes()
|
|
return fmt.Errorf("%v: %w (%s)", args, err, string(out))
|
|
}
|
|
}
|
|
c.routesAdded = true
|
|
return nil
|
|
}
|
|
|
|
func (c *TUNClient) removeRoutes() {
|
|
if !c.routesAdded && len(c.bypassIPs) == 0 {
|
|
return
|
|
}
|
|
exec.Command("sudo", "route", "delete", "-net", "0.0.0.0/1").Run()
|
|
exec.Command("sudo", "route", "delete", "-net", "128.0.0.0/1").Run()
|
|
for _, ip := range c.bypassIPs {
|
|
exec.Command("sudo", "route", "delete", "-host", ip).Run()
|
|
}
|
|
c.bypassIPs = nil
|
|
c.routesAdded = false
|
|
}
|
|
|
|
func (c *TUNClient) Start() {
|
|
go c.readFromTun()
|
|
go c.writeToTun()
|
|
|
|
c.trans.Receive(func(pkt []byte) {
|
|
cp := make([]byte, len(pkt))
|
|
copy(cp, pkt)
|
|
select {
|
|
case c.inbound <- cp:
|
|
default:
|
|
utils.Debugf("[TUN] inbound queue full, dropping")
|
|
}
|
|
})
|
|
}
|
|
|
|
func (c *TUNClient) readFromTun() {
|
|
utils.Debugf("[TUN] readFromTun started, fd=%v name=%s", c.fd.Fd(), c.name)
|
|
buf := make([]byte, 2048)
|
|
for {
|
|
n, err := c.fd.Read(buf)
|
|
if err != nil {
|
|
utils.Debugf("[TUN] read: %v", err)
|
|
return
|
|
}
|
|
if n < 4 {
|
|
continue
|
|
}
|
|
pkt := make([]byte, n-4)
|
|
copy(pkt, buf[4:n])
|
|
if len(pkt) < 20 || pkt[0]>>4 != 4 {
|
|
utils.Debugf("[TUN] not IPv4, skipping (%d bytes)", len(pkt))
|
|
continue
|
|
}
|
|
c.packetsOut.Add(1)
|
|
|
|
// -> : packet leaves the device towards the tunnel / exit.
|
|
network.LogPacket("TUN", network.DirOutbound, pkt)
|
|
|
|
if err := c.trans.Send(pkt); err != nil {
|
|
utils.Debugf("[TUN] trans.Send FAIL: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *TUNClient) writeToTun() {
|
|
for pkt := range c.inbound {
|
|
c.packetsIn.Add(1)
|
|
|
|
// <- : packet arrives from the tunnel / exit towards the device.
|
|
network.LogPacket("TUN", network.DirInbound, pkt)
|
|
|
|
out := make([]byte, 4+len(pkt))
|
|
out[3] = 2 // AF_INET
|
|
copy(out[4:], pkt)
|
|
if _, err := c.fd.Write(out); err != nil {
|
|
utils.Debugf("[TUN] write: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *TUNClient) Close() error {
|
|
c.removeRoutes()
|
|
exec.Command("sudo", "ifconfig", c.name, "down").Run()
|
|
return c.fd.Close()
|
|
}
|
|
|
|
func openUtun() (*os.File, string, error) {
|
|
fd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("socket AF_SYSTEM: %w", err)
|
|
}
|
|
|
|
var info unix.CtlInfo
|
|
copy(info.Name[:], "com.apple.net.utun_control")
|
|
if err := unix.IoctlCtlInfo(fd, &info); err != nil {
|
|
unix.Close(fd)
|
|
return nil, "", fmt.Errorf("IoctlCtlInfo: %w", err)
|
|
}
|
|
|
|
sa := &unix.SockaddrCtl{
|
|
ID: info.Id,
|
|
Unit: 0,
|
|
}
|
|
if err := unix.Connect(fd, sa); err != nil {
|
|
unix.Close(fd)
|
|
return nil, "", fmt.Errorf("connect PF_SYSTEM: %w", err)
|
|
}
|
|
|
|
name, err := unix.GetsockoptString(fd, 2, 2)
|
|
if err != nil {
|
|
unix.Close(fd)
|
|
return nil, "", fmt.Errorf("getsockopt UTUN_OPT_IFNAME: %w", err)
|
|
}
|
|
|
|
return os.NewFile(uintptr(fd), name), name, nil
|
|
}
|
|
|
|
var _ = net.IPv4len
|
|
|
|
func realGateway() (string, string, error) {
|
|
out, err := exec.Command("sh", "-c", "ifconfig -l").Output()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("ifconfig -l: %w", err)
|
|
}
|
|
names := strings.Fields(string(out))
|
|
for _, name := range names {
|
|
if !strings.HasPrefix(name, "en") {
|
|
continue
|
|
}
|
|
ifout, err := exec.Command("ifconfig", name).Output()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ip, mask := parseIfconfigIPv4(string(ifout))
|
|
if ip == nil || mask == nil {
|
|
continue
|
|
}
|
|
gw := firstUsableHost(ip, mask)
|
|
if gw == "" {
|
|
continue
|
|
}
|
|
return gw, name, nil
|
|
}
|
|
return "", "", fmt.Errorf("no physical interface with IPv4 found")
|
|
}
|
|
|
|
func parseIfconfigIPv4(s string) (net.IP, net.IPMask) {
|
|
for _, line := range strings.Split(s, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if !strings.HasPrefix(line, "inet ") {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
var ip net.IP
|
|
var mask net.IPMask
|
|
for i := 0; i < len(fields); i++ {
|
|
switch fields[i] {
|
|
case "inet":
|
|
if i+1 < len(fields) {
|
|
ip = net.ParseIP(fields[i+1]).To4()
|
|
}
|
|
case "netmask":
|
|
if i+1 < len(fields) {
|
|
mask = parseMask(fields[i+1])
|
|
}
|
|
}
|
|
}
|
|
if ip != nil && mask != nil {
|
|
return ip, mask
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func parseMask(s string) net.IPMask {
|
|
if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") {
|
|
var m uint32
|
|
if _, err := fmt.Sscanf(s, "0x%x", &m); err == nil {
|
|
return net.IPv4Mask(byte(m>>24), byte(m>>16), byte(m>>8), byte(m))
|
|
}
|
|
return nil
|
|
}
|
|
if ip := net.ParseIP(s).To4(); ip != nil {
|
|
return net.IPMask(ip)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func firstUsableHost(ip net.IP, mask net.IPMask) string {
|
|
ip4 := ip.To4()
|
|
if ip4 == nil {
|
|
return ""
|
|
}
|
|
network := ip4.Mask(mask)
|
|
if len(network) == 4 {
|
|
network[3]++
|
|
}
|
|
ones, bits := mask.Size()
|
|
if bits == 0 || ones >= 31 {
|
|
return ""
|
|
}
|
|
return network.String()
|
|
}
|
|
|
|
func (c *TUNClient) Gateway() string { return c.gateway }
|
|
|
|
func (c *TUNClient) SaveDefault() error {
|
|
out, err := exec.Command("route", "-n", "get", "default").Output()
|
|
if err != nil {
|
|
return fmt.Errorf("route get default: %w", err)
|
|
}
|
|
var iface, gw string
|
|
for _, line := range strings.Split(string(out), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "interface:") {
|
|
iface = strings.TrimSpace(strings.TrimPrefix(line, "interface:"))
|
|
}
|
|
if strings.HasPrefix(line, "gateway:") {
|
|
gw = strings.TrimSpace(strings.TrimPrefix(line, "gateway:"))
|
|
}
|
|
}
|
|
c.savedIface = iface
|
|
c.savedGw = gw
|
|
utils.Debugf("[TUN] saved default: iface=%s gw=%s", iface, gw)
|
|
if iface == "" {
|
|
return fmt.Errorf("could not parse default interface from route output")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *TUNClient) SetupInterface() error {
|
|
exec.Command("sudo", "route", "delete", "-net", "0.0.0.0/1").Run()
|
|
exec.Command("sudo", "route", "delete", "-net", "128.0.0.0/1").Run()
|
|
|
|
for _, args := range [][]string{
|
|
{"ifconfig", c.name, "10.10.10.2", "10.10.10.2", "up"},
|
|
{"ifconfig", c.name, "mtu", "1280"},
|
|
} {
|
|
if out, err := exec.Command("sudo", args...).CombinedOutput(); err != nil {
|
|
return fmt.Errorf("%v: %w (%s)", args, err, string(out))
|
|
}
|
|
}
|
|
|
|
if c.savedGw != "" && net.ParseIP(c.savedGw) != nil {
|
|
c.gateway = c.savedGw
|
|
utils.Debugf("[TUN] bypass gateway = saved default gw %s", c.gateway)
|
|
return nil
|
|
}
|
|
gw, iface, err := realGateway()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.gateway = gw
|
|
utils.Debugf("[TUN] bypass gateway = realGateway() %s (iface=%s)", gw, iface)
|
|
return nil
|
|
}
|
|
|
|
func (c *TUNClient) RestoreDefault() {
|
|
if !c.defaultSet && c.savedIface == "" {
|
|
return
|
|
}
|
|
exec.Command("sudo", "route", "delete", "-net", "0.0.0.0/1").Run()
|
|
exec.Command("sudo", "route", "delete", "-net", "128.0.0.0/1").Run()
|
|
c.routesAdded = false
|
|
|
|
if c.savedIface == "" {
|
|
return
|
|
}
|
|
if c.savedGw != "" && net.ParseIP(c.savedGw) != nil {
|
|
exec.Command("sudo", "route", "add", "default", c.savedGw).Run()
|
|
} else {
|
|
exec.Command("sudo", "route", "add", "default", "-interface", c.savedIface).Run()
|
|
}
|
|
utils.Debugf("[TUN] default restored: iface=%s gw=%s", c.savedIface, c.savedGw)
|
|
c.defaultSet = false
|
|
}
|
|
|
|
func (c *TUNClient) ConfigureDefault() error {
|
|
for _, args := range [][]string{
|
|
{"route", "add", "-net", "0.0.0.0/1", "-interface", c.name},
|
|
{"route", "add", "-net", "128.0.0.0/1", "-interface", c.name},
|
|
} {
|
|
full := append([]string{"sudo"}, args...)
|
|
out, err := exec.Command(full[0], full[1:]...).CombinedOutput()
|
|
utils.Debugf("[TUN] exec %v -> err=%v out=%q", args, err, string(out))
|
|
if err != nil {
|
|
c.removeRoutes()
|
|
return fmt.Errorf("%v: %w (%s)", args, err, string(out))
|
|
}
|
|
}
|
|
c.routesAdded = true
|
|
c.defaultSet = true
|
|
utils.Debugf("[TUN] default routes installed")
|
|
return nil
|
|
}
|