//go:build darwin && !ios package net import ( stdnet "net" "net/netip" "os" "testing" "time" "unsafe" "golang.org/x/sys/unix" ) func TestFindProcessDarwinTCP(t *testing.T) { listener, err := stdnet.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() accepted := make(chan stdnet.Conn, 1) go func() { conn, err := listener.Accept() if err == nil { accepted <- conn return } close(accepted) }() conn, err := stdnet.Dial("tcp", listener.Addr().String()) if err != nil { t.Fatal(err) } defer conn.Close() serverConn := <-accepted if serverConn == nil { t.Fatal("server did not accept tcp connection") } defer serverConn.Close() local := conn.LocalAddr().(*stdnet.TCPAddr) remote := conn.RemoteAddr().(*stdnet.TCPAddr) pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), remote.IP.String(), uint16(remote.Port)) if err != nil { t.Fatal(err) } assertCurrentProcess(t, pid, name, path) } func TestFindProcessDarwinTCPIPv4Mapped(t *testing.T) { listener, err := stdnet.Listen("tcp4", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() accepted := make(chan stdnet.Conn, 1) go func() { conn, err := listener.Accept() if err == nil { accepted <- conn return } close(accepted) }() listenerAddr := listener.Addr().(*stdnet.TCPAddr) fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_STREAM, unix.IPPROTO_TCP) if err != nil { t.Fatal(err) } defer unix.Close(fd) mappedAddr := [16]byte{10: 0xff, 11: 0xff, 12: 127, 15: 1} if err := unix.Connect(fd, &unix.SockaddrInet6{ Port: listenerAddr.Port, Addr: mappedAddr, }); err != nil { t.Fatal(err) } serverConn := <-accepted if serverConn == nil { t.Fatal("server did not accept tcp connection") } defer serverConn.Close() local, err := unix.Getsockname(fd) if err != nil { t.Fatal(err) } localPort := local.(*unix.SockaddrInet6).Port pid, name, path, err := FindProcess("tcp", "127.0.0.1", uint16(localPort), "127.0.0.1", uint16(listenerAddr.Port)) if err != nil { t.Fatal(err) } assertCurrentProcess(t, pid, name, path) } func TestFindProcessDarwinUDP(t *testing.T) { conn, err := stdnet.ListenUDP("udp", &stdnet.UDPAddr{IP: stdnet.ParseIP("127.0.0.1")}) if err != nil { t.Fatal(err) } defer conn.Close() local := conn.LocalAddr().(*stdnet.UDPAddr) pid, name, path, err := FindProcess("udp", local.IP.String(), uint16(local.Port), "", 0) if err != nil { t.Fatal(err) } assertCurrentProcess(t, pid, name, path) } func TestFindProcessDarwinNonLocal(t *testing.T) { _, _, _, err := FindProcess("tcp", "203.0.113.1", 80, "", 0) if err != ErrNotLocal { t.Fatalf("expected ErrNotLocal, got %v", err) } } func TestFindProcessDarwinUnsupportedNetwork(t *testing.T) { defer func() { if recover() == nil { t.Fatal("expected panic") } }() _, _, _, _ = FindProcess("icmp", "127.0.0.1", 0, "", 0) } func assertCurrentProcess(t *testing.T, pid int, name string, path string) { t.Helper() if pid != os.Getpid() { t.Fatalf("expected pid %d, got %d (%s, %s)", os.Getpid(), pid, name, path) } executable, err := os.Executable() if err != nil { t.Fatal(err) } if path == "" || name == "" { t.Fatalf("expected process path and name, got name=%q path=%q", name, path) } if sameFile(executable, path) { return } t.Fatalf("expected executable %q, got %q", executable, path) } func sameFile(left string, right string) bool { leftInfo, leftErr := os.Stat(left) rightInfo, rightErr := os.Stat(right) if leftErr != nil || rightErr != nil { return false } return os.SameFile(leftInfo, rightInfo) } func TestFindProcessDarwinTCPWithoutDestination(t *testing.T) { listener, err := stdnet.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() accepted := make(chan stdnet.Conn, 1) go func() { conn, err := listener.Accept() if err == nil { accepted <- conn return } close(accepted) }() conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second) if err != nil { t.Fatal(err) } defer conn.Close() serverConn := <-accepted if serverConn == nil { t.Fatal("server did not accept tcp connection") } defer serverConn.Close() local := conn.LocalAddr().(*stdnet.TCPAddr) pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "", 0) if err != nil { t.Fatal(err) } assertCurrentProcess(t, pid, name, path) } func TestFindProcessDarwinTCPWithDifferentDestination(t *testing.T) { listener, err := stdnet.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() accepted := make(chan stdnet.Conn, 1) go func() { conn, err := listener.Accept() if err == nil { accepted <- conn return } close(accepted) }() conn, err := stdnet.DialTimeout("tcp", listener.Addr().String(), time.Second) if err != nil { t.Fatal(err) } defer conn.Close() serverConn := <-accepted if serverConn == nil { t.Fatal("server did not accept tcp connection") } defer serverConn.Close() local := conn.LocalAddr().(*stdnet.TCPAddr) pid, name, path, err := FindProcess("tcp", local.IP.String(), uint16(local.Port), "203.0.113.10", 443) if err != nil { t.Fatal(err) } assertCurrentProcess(t, pid, name, path) } func TestDarwinSocketInfoMatchLevelFallbacks(t *testing.T) { src := netip.MustParseAddr("198.18.0.2") dst := netip.MustParseAddr("203.0.113.10") otherLocal := netip.MustParseAddr("192.168.1.10") otherRemote := netip.MustParseAddr("198.51.100.10") unspecifiedLocal := netip.MustParseAddr("0.0.0.0") tests := []struct { name string local netip.Addr remote netip.Addr hasDst bool wantLevel darwinSocketMatchLevel }{ { name: "exact", local: src, remote: dst, hasDst: true, wantLevel: darwinSocketExactMatch, }, { name: "unspecified local with matching remote", local: unspecifiedLocal, remote: dst, hasDst: true, wantLevel: darwinSocketExactMatch, }, { name: "unspecified local without destination", local: unspecifiedLocal, remote: otherRemote, hasDst: false, wantLevel: darwinSocketExactMatch, }, { name: "local match with different remote", local: src, remote: otherRemote, hasDst: true, wantLevel: darwinSocketLocalMatch, }, { name: "remote match with different local", local: otherLocal, remote: dst, hasDst: true, wantLevel: darwinSocketRemoteMatch, }, { name: "port only with destination", local: otherLocal, remote: otherRemote, hasDst: true, wantLevel: darwinSocketPortMatch, }, { name: "different local without destination", local: otherLocal, remote: otherRemote, hasDst: false, wantLevel: darwinSocketNoMatch, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { info := newDarwinSocketInfo("tcp", test.local, 12345, test.remote, 443) level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, test.hasDst) if level != test.wantLevel { t.Fatalf("unexpected match level: got %d, want %d", level, test.wantLevel) } }) } } func TestDarwinSocketInfoMatchLevelIPv4Mapped(t *testing.T) { src := netip.MustParseAddr("127.0.0.1") dst := netip.MustParseAddr("203.0.113.10") info := newDarwinSocketInfo("tcp", src, 12345, dst, 443) writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET6)) level := darwinSocketInfoMatchLevel(info, "tcp", src, 12345, dst, 443, true) if level != darwinSocketExactMatch { t.Fatalf("unexpected match level: got %d, want %d", level, darwinSocketExactMatch) } } func newDarwinSocketInfo(network string, local netip.Addr, localPort uint16, remote netip.Addr, remotePort uint16) []byte { info := make([]byte, darwinSocketFDInfoSize) switch network { case "tcp": writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_TCP)) writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoTCP)) case "udp": writeDarwinNativeUint32(info, darwinSocketInfoProtoOff, uint32(unix.IPPROTO_UDP)) writeDarwinNativeUint32(info, darwinSocketInfoKindOff, uint32(darwinSockInfoIN)) } writeDarwinNativeUint32(info, darwinSocketInfoFamilyOff, uint32(unix.AF_INET)) info[darwinInSockInfoVFlagOff] = darwinInSockInfoIPv4 writeDarwinNativeUint32(info, darwinInSockInfoLPortOff, uint32(localPort)) writeDarwinNativeUint32(info, darwinInSockInfoFPortOff, uint32(remotePort)) copyDarwinIPv4(info[darwinInSockInfoLAddrOff:darwinInSockInfoLAddrOff+16], local) copyDarwinIPv4(info[darwinInSockInfoFAddrOff:darwinInSockInfoFAddrOff+16], remote) return info } func writeDarwinNativeUint32(b []byte, offset int, value uint32) { *(*uint32)(unsafe.Pointer(&b[offset])) = value } func copyDarwinIPv4(dst []byte, addr netip.Addr) { ip := addr.As4() copy(dst[12:16], ip[:]) }