├── testdata
├── llmnr_request.bin
├── mdns_request.bin
├── dns_reply.bin
├── dns_request.bin
├── llmnr_reply.bin
├── mdns_reply.bin
├── dhcpv6_renew.bin
├── netbios_reply.bin
├── dhcpv6_advertise.bin
├── dhcpv6_request.bin
├── dhcpv6_solicit.bin
├── netbios_request.bin
├── dhcpv6_renew_reply.bin
└── dhcpv6_request_reply.bin
├── .gitignore
├── hostinfo
├── testdata
│ ├── unified
│ │ ├── mac-vendors.txt
│ │ ├── linux_proc_net_arp
│ │ ├── linux_ip_neighbor
│ │ ├── windows_arp
│ │ └── windows_netsh_show_neighbors
│ ├── linux_proc_net_arp
│ ├── linux_ip_neighbor
│ ├── windows_arp
│ └── windows_netsh_show_neighbors
├── ip_neighbor.go
├── ip_neighbor_test.go
├── netsh_neighbors.go
├── netsh_neighbors_test.go
├── arp_test.go
├── generate_mac_vendors.py
├── arp.go
├── host_info_test.go
└── host_info.go
├── go.mod
├── workspace.code-workspace
├── .github
└── workflows
│ ├── release.yml
│ └── check.yml
├── .goreleaser.yaml
├── LICENSE
├── multicast.go
├── router_advertisement.go
├── main.go
├── version.go
├── .golangci.yml
├── dhcpv6_test.go
├── defaults.go
├── local_name_resolution_test.go
├── filter.go
├── README.md
├── log.go
├── dns_test.go
├── go.sum
├── dns.go
├── filter_test.go
├── local_name_resolution.go
├── dhcpv6.go
└── cli.go
/testdata/llmnr_request.bin:
--------------------------------------------------------------------------------
1 | test
--------------------------------------------------------------------------------
/testdata/mdns_request.bin:
--------------------------------------------------------------------------------
1 | testlocal
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pretender
2 | pretender.exe
3 | Vagrantfile
4 | .vagrant
5 | .sshcfg
6 | dist/
7 |
--------------------------------------------------------------------------------
/testdata/dns_reply.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/dns_reply.bin
--------------------------------------------------------------------------------
/testdata/dns_request.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/dns_request.bin
--------------------------------------------------------------------------------
/testdata/llmnr_reply.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/llmnr_reply.bin
--------------------------------------------------------------------------------
/testdata/mdns_reply.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/mdns_reply.bin
--------------------------------------------------------------------------------
/testdata/dhcpv6_renew.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/dhcpv6_renew.bin
--------------------------------------------------------------------------------
/testdata/netbios_reply.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/netbios_reply.bin
--------------------------------------------------------------------------------
/testdata/dhcpv6_advertise.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/dhcpv6_advertise.bin
--------------------------------------------------------------------------------
/testdata/dhcpv6_request.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/dhcpv6_request.bin
--------------------------------------------------------------------------------
/testdata/dhcpv6_solicit.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/dhcpv6_solicit.bin
--------------------------------------------------------------------------------
/testdata/netbios_request.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/netbios_request.bin
--------------------------------------------------------------------------------
/testdata/dhcpv6_renew_reply.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/dhcpv6_renew_reply.bin
--------------------------------------------------------------------------------
/testdata/dhcpv6_request_reply.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/e1abrador/pretender/main/testdata/dhcpv6_request_reply.bin
--------------------------------------------------------------------------------
/hostinfo/testdata/unified/mac-vendors.txt:
--------------------------------------------------------------------------------
1 | # comment
2 | 08:00:27 A
3 | 08:00:27:FF specific
4 | 0a:00:27 test
5 | 52:54:00 B Test Comment
6 | ff:ff:ff C
7 |
8 |
--------------------------------------------------------------------------------
/hostinfo/testdata/linux_proc_net_arp:
--------------------------------------------------------------------------------
1 | IP address HW type Flags HW address Mask Device
2 | 192.168.56.101 0x1 0x2 08:00:27:56:c2:4c * eth1
3 | 10.0.2.3 0x1 0x2 52:54:00:12:35:03 * eth0
4 | 192.168.56.100 0x1 0x2 0a:00:27:00:00:00 * eth1
5 | 10.0.2.2 0x1 0x2 52:54:00:12:35:02 * eth0
6 |
--------------------------------------------------------------------------------
/hostinfo/testdata/linux_ip_neighbor:
--------------------------------------------------------------------------------
1 | 192.168.56.101 dev eth1 lladdr 08:00:27:56:c2:4c STALE
2 | 192.168.56.2 dev eth1 lladdr 08:00:27:c2:e0:27 STALE
3 | 10.0.2.3 dev eth0 lladdr 52:54:00:12:35:03 STALE
4 | 192.168.56.100 dev eth1 lladdr 0a:00:27:00:00:00 STALE
5 | 10.0.2.2 dev eth0 lladdr 52:54:00:12:35:02 REACHABLE
6 | fe80::a00:27ff:fe7e:ca64 dev eth1 lladdr 08:00:27:7e:ca:64 router STALE
7 | fe80::d422:2ab:8bf4:7381 dev eth1 lladdr 08:00:27:56:c2:4c STALE
8 | fe80::800:27ff:fe00:0 dev eth1 lladdr 0a:00:27:00:00:00 STALE
9 |
--------------------------------------------------------------------------------
/hostinfo/testdata/unified/linux_proc_net_arp:
--------------------------------------------------------------------------------
1 | IP address HW type Flags HW address Mask Device
2 | 192.168.56.101 0x1 0x2 08:00:27:56:c2:4c * eth1
3 | 192.168.56.2 0x1 0x2 08:00:27:c2:e0:27 * eth1
4 | 10.0.2.3 0x1 0x2 52:54:00:12:35:03 * eth0
5 | 192.168.56.100 0x1 0x2 0a:00:27:00:00:00 * eth1
6 | 10.0.2.2 0x1 0x2 52:54:00:12:35:02 * eth0
7 |
--------------------------------------------------------------------------------
/hostinfo/testdata/unified/linux_ip_neighbor:
--------------------------------------------------------------------------------
1 | 192.168.56.101 dev eth1 lladdr 08:00:27:56:c2:4c STALE
2 | 192.168.56.2 dev eth1 lladdr 08:00:27:c2:e0:27 STALE
3 | 10.0.2.3 dev eth0 lladdr 52:54:00:12:35:03 STALE
4 | 192.168.56.100 dev eth1 lladdr 0a:00:27:00:00:00 STALE
5 | 10.0.2.2 dev eth0 lladdr 52:54:00:12:35:02 REACHABLE
6 | fe80::a00:27ff:fe7e:ca64 dev eth1 lladdr 08:00:27:7e:ca:64 router STALE
7 | fe80::d422:2ab:8bf4:7381 dev eth1 lladdr 08:00:27:56:c2:4c STALE
8 | fe80::800:27ff:fe00:0 dev eth1 lladdr 0a:00:27:00:00:00 STALE
9 |
--------------------------------------------------------------------------------
/hostinfo/testdata/unified/windows_arp:
--------------------------------------------------------------------------------
1 |
2 | Interface: 10.0.2.15 --- 0xc
3 | Internet Address Physical Address Type
4 | 10.0.2.2 52-54-00-12-35-02 dynamic
5 | 10.0.2.3 52-54-00-12-35-03 dynamic
6 |
7 | Interface: 192.168.56.9 --- 0xd
8 | Internet Address Physical Address Type
9 | 192.168.56.101 08-00-27-56-c2-4c dynamic
10 | 192.168.56.2 08-00-27-c2-e0-27 dynamic
11 | 192.168.56.100 0a-00-27-00-00-00 dynamic
12 |
13 |
14 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/RedTeamPentesting/pretender
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c
7 | github.com/mdlayher/ndp v1.0.0
8 | github.com/miekg/dns v1.1.50
9 | github.com/spf13/pflag v1.0.5
10 | golang.org/x/net v0.1.0
11 | golang.org/x/sync v0.1.0
12 | )
13 |
14 | require (
15 | github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
16 | golang.org/x/mod v0.6.0 // indirect
17 | golang.org/x/sys v0.2.0 // indirect
18 | golang.org/x/text v0.4.0 // indirect
19 | golang.org/x/tools v0.2.0 // indirect
20 | )
21 |
--------------------------------------------------------------------------------
/workspace.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {
8 | "go.useLanguageServer": true,
9 | "go.lintTool": "golangci-lint",
10 | "gopls": {
11 | "gofumpt": true,
12 | },
13 | "go.formatTool": "gofumports",
14 | "[go]": {
15 | "editor.formatOnSave": true,
16 | "editor.codeActionsOnSave": {
17 | "source.organizeImports": true,
18 | },
19 | },
20 | "[go.mod]": {
21 | "editor.formatOnSave": true,
22 | "editor.codeActionsOnSave": {
23 | "source.organizeImports": true,
24 | },
25 | }
26 | },
27 | "extensions": {
28 | "recommendations": [
29 | "golang.go"
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | goreleaser:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3
14 | with:
15 | fetch-depth: 0
16 |
17 | - name: Set up Go
18 | uses: actions/setup-go@v3
19 | with:
20 | go-version: 1.19.x
21 |
22 | - name: Run GoReleaser
23 | uses: goreleaser/goreleaser-action@v3
24 | with:
25 | distribution: goreleaser
26 | version: latest
27 | args: release --rm-dist
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 |
2 | before:
3 | hooks:
4 | - go mod tidy
5 |
6 | builds:
7 | -
8 | ldflags:
9 | - -s -w -X main.version=v{{.Version}}
10 | env:
11 | - CGO_ENABLED=0
12 | targets:
13 | - linux_amd64
14 | - linux_arm64
15 | - linux_arm
16 | - darwin_amd64
17 | - darwin_arm64
18 | - windows_amd64
19 |
20 | archives:
21 | -
22 | replacements:
23 | darwin: macOS
24 | linux: Linux
25 | windows: Windows
26 | amd64: x86_64
27 | format_overrides:
28 | - goos: windows
29 | format: zip
30 |
31 | checksum:
32 | name_template: 'checksums.txt'
33 |
34 | snapshot:
35 | name_template: "{{ incpatch .Version }}-dev"
36 |
37 | changelog:
38 | sort: asc
39 | filters:
40 | exclude:
41 | - '^docs:'
42 | - '^test:'
43 |
--------------------------------------------------------------------------------
/hostinfo/testdata/windows_arp:
--------------------------------------------------------------------------------
1 |
2 | Interface: 10.0.2.15 --- 0xc
3 | Internet Address Physical Address Type
4 | 10.0.2.2 52-54-00-12-35-02 dynamic
5 | 10.0.2.3 52-54-00-12-35-03 dynamic
6 | 10.0.2.255 ff-ff-ff-ff-ff-ff static
7 | 224.0.0.22 01-00-5e-00-00-16 static
8 | 224.0.0.251 01-00-5e-00-00-fb static
9 | 224.0.0.252 01-00-5e-00-00-fc static
10 | 239.255.255.250 01-00-5e-7f-ff-fa static
11 | 255.255.255.255 ff-ff-ff-ff-ff-ff static
12 |
13 | Interface: 192.168.56.101 --- 0xd
14 | Internet Address Physical Address Type
15 | 192.168.56.9 08-00-27-7e-ca-64 dynamic
16 | 192.168.56.255 ff-ff-ff-ff-ff-ff static
17 | 224.0.0.22 01-00-5e-00-00-16 static
18 | 224.0.0.251 01-00-5e-00-00-fb static
19 | 239.255.255.250 01-00-5e-7f-ff-fa static
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2021 RedTeam Pentesting GmbH
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 |
--------------------------------------------------------------------------------
/multicast.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "runtime"
7 |
8 | "golang.org/x/net/ipv6"
9 | )
10 |
11 | const osWindows = "windows"
12 |
13 | // ListenUDPMulticast listens on a multicast group in a way that is supported on
14 | // Unix and Windows for both IPv4 and IPv6.
15 | func ListenUDPMulticast(iface *net.Interface, multicastGroup *net.UDPAddr) (net.PacketConn, error) {
16 | if multicastGroup.IP.To4() != nil {
17 | return net.ListenMulticastUDP("udp", iface, multicastGroup)
18 | }
19 |
20 | if runtime.GOOS != osWindows {
21 | return net.ListenMulticastUDP("udp6", iface, multicastGroup)
22 | }
23 |
24 | listenAddr := &net.UDPAddr{IP: multicastGroup.IP, Port: multicastGroup.Port}
25 |
26 | conn, err := net.ListenPacket("udp6", listenAddr.String())
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | packetConn := ipv6.NewPacketConn(conn)
32 |
33 | err = packetConn.JoinGroup(iface, listenAddr)
34 | if err != nil {
35 | return nil, fmt.Errorf("join multicast group %s: %w", listenAddr.IP, err)
36 | }
37 |
38 | return conn, nil
39 | }
40 |
--------------------------------------------------------------------------------
/hostinfo/ip_neighbor.go:
--------------------------------------------------------------------------------
1 | package hostinfo
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "net"
7 | "runtime"
8 | "strings"
9 | )
10 |
11 | var linuxIPCommandAvailable = commandAvailable("ip", osLinux)
12 |
13 | func getMACFromLinuxIPNeighbor(ip net.IP) net.HardwareAddr {
14 | if !linuxIPCommandAvailable || runtime.GOOS != osLinux {
15 | return nil
16 | }
17 |
18 | if testMode {
19 | return getMacFromLinuxIPNeighborOutput(ip, readFileIfPossible("testdata/unified/linux_ip_neighbor"))
20 | }
21 |
22 | return getMacFromLinuxIPNeighborOutput(ip, readOutput(execTimeoutLinux, "ip", "neighbor"))
23 | }
24 |
25 | func getMacFromLinuxIPNeighborOutput(ip net.IP, ipNeighOutput []byte) net.HardwareAddr {
26 | if ip == nil || ipNeighOutput == nil {
27 | return nil
28 | }
29 |
30 | scanner := bufio.NewScanner(bytes.NewReader(ipNeighOutput))
31 | for scanner.Scan() {
32 | parts := strings.SplitN(scanner.Text(), " ", 6) //nolint:gomnd
33 | if len(parts) < 6 { //nolint:gomnd
34 | continue
35 | }
36 |
37 | if !ip.Equal(net.ParseIP(parts[0])) {
38 | continue
39 | }
40 |
41 | mac, err := net.ParseMAC(parts[4])
42 | if err == nil {
43 | return mac
44 | }
45 | }
46 |
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/hostinfo/testdata/unified/windows_netsh_show_neighbors:
--------------------------------------------------------------------------------
1 |
2 | Interface 1: Loopback Pseudo-Interface 1
3 |
4 |
5 | Internet Address Physical Address Type
6 | -------------------------------------------- ----------------- -----------
7 | ff02::c Permanent
8 | ff02::16 Permanent
9 | ff02::1:2 Permanent
10 |
11 | Interface 12: Ethernet
12 |
13 |
14 | Internet Address Physical Address Type
15 | -------------------------------------------- ----------------- -----------
16 |
17 |
18 |
19 | Interface 13: Ethernet 2
20 |
21 |
22 | Internet Address Physical Address Type
23 | -------------------------------------------- ----------------- -----------
24 | fe80::a00:27ff:fe7e:ca64 08-00-27-7e-ca-64 Stale
25 | fe80::a00:27ff:fe7e:ca64 08-00-27-7e-ca-64 Stale
26 | fe80::a00:27ff:fe7e:ca64 08-00-27-7e-ca-64 Stale
27 | fe80::d422:2ab:8bf4:7381 08-00-27-56-c2-4c Stale
28 | fe80::800:27ff:fe00:0 0a-00-27-00-00-00 Stale
29 |
--------------------------------------------------------------------------------
/hostinfo/ip_neighbor_test.go:
--------------------------------------------------------------------------------
1 | package hostinfo
2 |
3 | import "testing"
4 |
5 | func TestGetMacFromLinuxIPNeighborOutput(t *testing.T) {
6 | testGetMac(t, "testdata/linux_ip_neighbor", getMacFromLinuxIPNeighborOutput, []macIPTestCase{
7 | {ip: "", mac: ""},
8 | {ip: "127.0.0.1", mac: ""},
9 | {ip: "192.168.56.2", mac: "08:00:27:c2:e0:27"},
10 | {ip: "10.0.2.3", mac: "52:54:00:12:35:03"},
11 | {ip: "192.168.56.100", mac: "0a:00:27:00:00:00"},
12 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
13 | {ip: "fe80::a00:27ff:fe7e:ca64", mac: "08:00:27:7e:ca:64"},
14 | {ip: "fe80::d422:2ab:8bf4:7381", mac: "08:00:27:56:c2:4c"},
15 | {ip: "fe80::800:27ff:fe00:0", mac: "0a:00:27:00:00:00"},
16 | })
17 |
18 | testGetMac(t, "testdata/unified/linux_ip_neighbor", getMacFromLinuxIPNeighborOutput, []macIPTestCase{
19 | {ip: "", mac: ""},
20 | {ip: "127.0.0.1", mac: ""},
21 | {ip: "192.168.56.2", mac: "08:00:27:c2:e0:27"},
22 | {ip: "10.0.2.3", mac: "52:54:00:12:35:03"},
23 | {ip: "192.168.56.100", mac: "0a:00:27:00:00:00"},
24 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
25 | {ip: "fe80::a00:27ff:fe7e:ca64", mac: "08:00:27:7e:ca:64"},
26 | {ip: "fe80::d422:2ab:8bf4:7381", mac: "08:00:27:56:c2:4c"},
27 | {ip: "fe80::800:27ff:fe00:0", mac: "0a:00:27:00:00:00"},
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/hostinfo/netsh_neighbors.go:
--------------------------------------------------------------------------------
1 | package hostinfo
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "net"
7 | "runtime"
8 | "strings"
9 | )
10 |
11 | var windowsNetshCommandAvailable = commandAvailable("netsh", osWindows)
12 |
13 | func getMACFromWindowsNetshShowNeighbors(ip net.IP) net.HardwareAddr {
14 | if !windowsNetshCommandAvailable || runtime.GOOS != osWindows {
15 | return nil
16 | }
17 |
18 | if testMode {
19 | return getMacFromLinuxIPNeighborOutput(ip, readFileIfPossible("testdata/unified/windows_netsh_show_neighbors"))
20 | }
21 |
22 | return getMACFromWindowsNetshShowNeighborsOutput(ip, readOutput(execTimeoutWindows,
23 | "netsh", "interface", "ipv6", "show", "neighbors"))
24 | }
25 |
26 | func getMACFromWindowsNetshShowNeighborsOutput(ip net.IP, netshOutput []byte) net.HardwareAddr {
27 | if ip == nil || netshOutput == nil {
28 | return nil
29 | }
30 |
31 | scanner := bufio.NewScanner(bytes.NewReader(netshOutput))
32 | for scanner.Scan() {
33 | parts := strings.Fields(scanner.Text())
34 | if len(parts) < 3 { //nolint:gomnd
35 | continue
36 | }
37 |
38 | if !ip.Equal(net.ParseIP(parts[0])) {
39 | continue
40 | }
41 |
42 | if parts[2] == "Unreachable" {
43 | continue
44 | }
45 |
46 | mac, err := net.ParseMAC(parts[1])
47 | if err == nil {
48 | return mac
49 | }
50 | }
51 |
52 | return nil
53 | }
54 |
--------------------------------------------------------------------------------
/hostinfo/netsh_neighbors_test.go:
--------------------------------------------------------------------------------
1 | package hostinfo
2 |
3 | import "testing"
4 |
5 | func TestGetMacFromWindowsNetshShowNeighborsOutput(t *testing.T) {
6 | testGetMac(t, "testdata/windows_netsh_show_neighbors", getMACFromWindowsNetshShowNeighborsOutput, []macIPTestCase{
7 | {ip: "", mac: ""},
8 | {ip: "127.0.0.1", mac: ""},
9 | {ip: "ff02::1", mac: "33-33-00-00-00-01"},
10 | {ip: "ff02::2", mac: "33-33-00-00-00-02"},
11 | {ip: "ff02::c", mac: "33-33-00-00-00-0c"},
12 | {ip: "ff02::16", mac: "33-33-00-00-00-16"},
13 | {ip: "ff02::fb", mac: "33-33-00-00-00-fb"},
14 | {ip: "ff02::1:2", mac: "33-33-00-01-00-02"},
15 | {ip: "ff02::1:3", mac: "33-33-00-01-00-03"},
16 | {ip: "ff02::1:ff49:f89a", mac: "33-33-ff-49-f8-9a"},
17 | {ip: "fe80::a00:27ff:fe7e:ca64", mac: "08-00-27-7e-ca-64"},
18 | {ip: "ff02::1:ff04:792b", mac: "33-33-ff-04-79-2b"},
19 | {ip: "ff02::1:ff7e:ca64", mac: "33-33-ff-7e-ca-64"},
20 | {ip: "ff02::1:fff4:7381", mac: "33-33-ff-f4-73-81"},
21 | })
22 |
23 | testGetMac(t, "testdata/unified/windows_netsh_show_neighbors",
24 | getMACFromWindowsNetshShowNeighborsOutput, []macIPTestCase{
25 | {ip: "", mac: ""},
26 | {ip: "127.0.0.1", mac: ""},
27 | {ip: "fe80::a00:27ff:fe7e:ca64", mac: "08-00-27-7e-ca-64"},
28 | {ip: "fe80::a00:27ff:fe7e:ca64", mac: "08-00-27-7e-ca-64"},
29 | {ip: "fe80::a00:27ff:fe7e:ca64", mac: "08-00-27-7e-ca-64"},
30 | {ip: "fe80::d422:2ab:8bf4:7381", mac: "08-00-27-56-c2-4c"},
31 | {ip: "fe80::800:27ff:fe00:0", mac: "0a-00-27-00-00-00"},
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Check
2 | on:
3 | # run tests on push to main, but not when other branches are pushed to
4 | push:
5 | branches:
6 | - main
7 |
8 | # run tests for all pull requests
9 | pull_request:
10 |
11 | jobs:
12 | lint:
13 | name: Lint
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Set up Go
17 | uses: actions/setup-go@v3
18 | with:
19 | go-version: 1.19.x
20 | id: go
21 |
22 | - name: Checkout code
23 | uses: actions/checkout@v3
24 |
25 | - name: golangci-lint
26 | uses: golangci/golangci-lint-action@v3
27 | with:
28 | version: v1.50
29 | args: --verbose --timeout 5m
30 |
31 | - name: Check go.mod/go.sum
32 | run: |
33 | echo "check if go.mod and go.sum are up to date"
34 | go mod tidy
35 | git diff --exit-code go.mod go.sum
36 |
37 | test:
38 | name: Test
39 | runs-on: ubuntu-latest
40 | steps:
41 | - name: Set up Go
42 | uses: actions/setup-go@v3
43 | with:
44 | go-version: 1.19.x
45 | id: go
46 |
47 | - name: Checkout code
48 | uses: actions/checkout@v3
49 |
50 | - name: Run tests
51 | run: |
52 | go test ./...
53 |
54 | build:
55 | strategy:
56 | matrix:
57 | go-version:
58 | - 1.19.x
59 | runs-on: ubuntu-latest
60 | name: Build with Go ${{ matrix.go-version }}
61 | env:
62 | GOPROXY: https://proxy.golang.org
63 |
64 | steps:
65 | - name: Set up Go ${{ matrix.go-version }}
66 | uses: actions/setup-go@v3
67 | with:
68 | go-version: ${{ matrix.go-version }}
69 | id: go
70 |
71 | - name: Checkout code
72 | uses: actions/checkout@v3
73 |
74 | - name: Build
75 | run: |
76 | GOOS=linux GOARCH=amd64 go build -o pretender_linux_amd64
77 | GOOS=linux GOARCH=arm64 go build -o pretender_linux_arm64
78 | GOOS=windows GOARCH=amd64 go build -o pretender_windows
79 | GOOS=darwin GOARCH=amd64 go build -o pretender_macos_amd64
80 | GOOS=darwin GOARCH=arm64 go build -o pretender_linux_arm64
81 |
--------------------------------------------------------------------------------
/hostinfo/testdata/windows_netsh_show_neighbors:
--------------------------------------------------------------------------------
1 |
2 | Interface 1: Loopback Pseudo-Interface 1
3 |
4 |
5 | Internet Address Physical Address Type
6 | -------------------------------------------- ----------------- -----------
7 | ff02::c Permanent
8 | ff02::16 Permanent
9 | ff02::1:2 Permanent
10 |
11 | Interface 12: Ethernet
12 |
13 |
14 | Internet Address Physical Address Type
15 | -------------------------------------------- ----------------- -----------
16 | ff02::1 33-33-00-00-00-01 Permanent
17 | ff02::2 33-33-00-00-00-02 Permanent
18 | ff02::c 33-33-00-00-00-0c Permanent
19 | ff02::16 33-33-00-00-00-16 Permanent
20 | ff02::fb 33-33-00-00-00-fb Permanent
21 | ff02::1:2 33-33-00-01-00-02 Permanent
22 | ff02::1:3 33-33-00-01-00-03 Permanent
23 | ff02::1:ff49:f89a 33-33-ff-49-f8-9a Permanent
24 |
25 | Interface 13: Ethernet 2
26 |
27 |
28 | Internet Address Physical Address Type
29 | -------------------------------------------- ----------------- -----------
30 | fe80::a00:27ff:fe7e:ca64 08-00-27-7e-ca-64 Stale
31 | ff02::1 33-33-00-00-00-01 Permanent
32 | ff02::2 33-33-00-00-00-02 Permanent
33 | ff02::c 33-33-00-00-00-0c Permanent
34 | ff02::16 33-33-00-00-00-16 Permanent
35 | ff02::fb 33-33-00-00-00-fb Permanent
36 | ff02::1:2 33-33-00-01-00-02 Permanent
37 | ff02::1:3 33-33-00-01-00-03 Permanent
38 | ff02::1:ff04:792b 33-33-ff-04-79-2b Permanent
39 | ff02::1:ff7e:ca64 33-33-ff-7e-ca-64 Permanent
40 | ff02::1:fff4:7381 33-33-ff-f4-73-81 Permanent
41 |
42 |
--------------------------------------------------------------------------------
/router_advertisement.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "net/netip"
8 | "time"
9 |
10 | "github.com/mdlayher/ndp"
11 | )
12 |
13 | const (
14 | raHopLimit = 0
15 | raDefaultRouterLifetime = 180 * time.Second
16 | raDelay = 500 * time.Millisecond
17 | raDefaultPeriod = 3 * time.Minute
18 | )
19 |
20 | var (
21 | ipv6LinkLocalAllRouters = netip.MustParseAddr(net.IPv6linklocalallrouters.String())
22 | ipv6LinkLocalAllNodes = netip.MustParseAddr(net.IPv6linklocalallnodes.String())
23 | )
24 |
25 | // SendPeriodicRouterAdvertisements sends periodic router advertisement messages.
26 | func SendPeriodicRouterAdvertisements(ctx context.Context, logger *Logger, config Config) error {
27 | iface, err := net.InterfaceByName(config.Interface.Name)
28 | if err != nil {
29 | return fmt.Errorf("selecting interface %q: %w", config.Interface.Name, err)
30 | }
31 |
32 | conn, _, err := ndp.Listen(config.Interface, ndp.LinkLocal)
33 | if err != nil {
34 | return fmt.Errorf("dialing (%s): %w", config.Interface.Name, err)
35 | }
36 |
37 | defer func() { _ = conn.Close() }()
38 |
39 | err = conn.JoinGroup(ipv6LinkLocalAllRouters)
40 | if err != nil {
41 | return fmt.Errorf("joining multicast group: %w", err)
42 | }
43 |
44 | time.Sleep(raDelay) // time for DHCPv6 server to start
45 |
46 | for {
47 | logger.Infof("sending router advertisement on %s", iface.Name)
48 |
49 | err := sendRouterAdvertisement(conn, iface.HardwareAddr, config.RouterLifetime)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | timer := time.NewTimer(config.RAPeriod)
55 |
56 | select {
57 | case <-ctx.Done():
58 | if !timer.Stop() {
59 | <-timer.C
60 | }
61 |
62 | if config.RouterLifetime > 0 {
63 | logger.Infof("sending router de-advertisement on %s", iface.Name)
64 |
65 | return sendRouterAdvertisement(conn, iface.HardwareAddr, 0)
66 | }
67 |
68 | return nil
69 | case <-timer.C:
70 | continue
71 | }
72 | }
73 | }
74 |
75 | func sendRouterAdvertisement(c *ndp.Conn, routerMAC net.HardwareAddr, routerLifetime time.Duration) error {
76 | m := &ndp.RouterAdvertisement{
77 | CurrentHopLimit: raHopLimit,
78 | ManagedConfiguration: true,
79 | OtherConfiguration: true,
80 |
81 | RouterSelectionPreference: ndp.High,
82 | RouterLifetime: routerLifetime,
83 | Options: []ndp.Option{
84 | &ndp.LinkLayerAddress{
85 | Direction: ndp.Source,
86 | Addr: routerMAC,
87 | },
88 | },
89 | }
90 |
91 | err := c.WriteTo(m, nil, ipv6LinkLocalAllNodes)
92 | if err != nil {
93 | return fmt.Errorf("sending router advertisement: %w", err)
94 | }
95 |
96 | return nil
97 | }
98 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | /****************************************
2 | * *
3 | * RedTeam Pentesting GmbH *
4 | * kontakt@redteam-pentesting.de *
5 | * https://www.redteam-pentesting.de/ *
6 | * *
7 | ****************************************/
8 | package main
9 |
10 | import (
11 | "context"
12 | "errors"
13 | "os"
14 | "os/signal"
15 | "sync"
16 | "syscall"
17 | )
18 |
19 | func main() {
20 | config, logger, err := configFromCLI()
21 | if err != nil {
22 | logger.Errorf("Error: " + err.Error())
23 | logger.Flush()
24 |
25 | if errors.As(err, &interfaceError{}) {
26 | logger.Errorf("Try specifying one of the following interfaces:")
27 | logger.Flush()
28 |
29 | _ = listInterfaces(stdErr, config.NoColor)
30 | }
31 |
32 | logger.Close()
33 |
34 | os.Exit(1)
35 | }
36 |
37 | runListeners(config, logger)
38 |
39 | logger.Close()
40 | }
41 |
42 | func runListeners(config Config, logger *Logger) { //nolint:cyclop
43 | ctx, cancel := signal.NotifyContext(context.Background(),
44 | os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
45 | defer cancel()
46 |
47 | if config.StopAfter > 0 {
48 | ctx, cancel = context.WithTimeout(ctx, config.StopAfter)
49 | defer cancel()
50 | }
51 |
52 | wg := newServiceWaitGroup(ctx)
53 |
54 | if !config.NoNetBIOS && !config.NoLocalNameResolution {
55 | wg.Run(RunNetBIOSResponder, logger.WithPrefix("NetBIOS"), config)
56 | }
57 |
58 | if !config.NoLLMNR && !config.NoLocalNameResolution {
59 | wg.Run(RunLLMNRResponder, logger.WithPrefix("LLMNR"), config)
60 | }
61 |
62 | if !config.NoMDNS && !config.NoLocalNameResolution {
63 | wg.Run(RunMDNSResponder, logger.WithPrefix("mDNS"), config)
64 | }
65 |
66 | if !config.NoDHCPv6DNSTakeover && !config.NoDNS {
67 | wg.Run(RunDNSResponder, logger.WithPrefix("DNS"), config)
68 | }
69 |
70 | if !config.NoDHCPv6DNSTakeover && !config.NoDHCPv6 {
71 | wg.Run(RunDHCPv6Server, logger.WithPrefix("DHCPv6"), config)
72 | }
73 |
74 | if !config.NoRA && !config.NoDHCPv6DNSTakeover && !config.NoDHCPv6 {
75 | wg.Run(SendPeriodicRouterAdvertisements, logger.WithPrefix("RA"), config)
76 | }
77 |
78 | wg.Wait()
79 | }
80 |
81 | type serviceFunc func(context.Context, *Logger, Config) error
82 |
83 | type serviceWaitGroup struct {
84 | ctx context.Context //nolint:containedctx
85 | sync.WaitGroup
86 | }
87 |
88 | func newServiceWaitGroup(ctx context.Context) *serviceWaitGroup {
89 | return &serviceWaitGroup{ctx: ctx}
90 | }
91 |
92 | func (wg *serviceWaitGroup) Run(service serviceFunc, logger *Logger, config Config) {
93 | wg.WaitGroup.Add(1)
94 |
95 | go func() {
96 | err := service(wg.ctx, logger, config)
97 | if err != nil {
98 | logger.Errorf(escapeFormatString(err.Error()))
99 | } else if wg.ctx.Err() != nil {
100 | logger.Debugf("shutdown")
101 | }
102 |
103 | wg.WaitGroup.Done()
104 | }()
105 | }
106 |
--------------------------------------------------------------------------------
/version.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "runtime/debug"
6 | "time"
7 | )
8 |
9 | const (
10 | banner = "Pretender by RedTeam Pentesting"
11 | shortCommitSize = 10
12 | )
13 |
14 | var version = "" // this variable can be set during compilation
15 |
16 | func buildSettingReader() func(string) (string, bool) {
17 | buildInfo, ok := debug.ReadBuildInfo()
18 | if !ok {
19 | return func(string) (string, bool) { return "", false }
20 | }
21 |
22 | return func(key string) (string, bool) {
23 | for _, setting := range buildInfo.Settings {
24 | if setting.Key == key {
25 | return setting.Value, true
26 | }
27 | }
28 |
29 | return "", false
30 | }
31 | }
32 |
33 | func shortVersion() string {
34 | fallback := version
35 | if fallback == "" {
36 | fallback = "(unknown version)"
37 | }
38 |
39 | readBuildSetting := buildSettingReader()
40 |
41 | commit, ok := readBuildSetting("vcs.revision")
42 | if !ok {
43 | return fmt.Sprintf("%s %s", banner, fallback)
44 | }
45 |
46 | if len(commit) > shortCommitSize {
47 | commit = commit[:shortCommitSize]
48 | }
49 |
50 | dirty, _ := readBuildSetting("vcs.modified")
51 |
52 | if version == "" {
53 | vcs, ok := readBuildSetting("vcs")
54 | if ok {
55 | vcs = " " + vcs
56 | }
57 |
58 | dirtySuffix := ""
59 | if dirty == "true" {
60 | dirtySuffix = " (dirty)"
61 | }
62 |
63 | return fmt.Sprintf("%s built from%s commit %s%s", banner, vcs, commit, dirtySuffix)
64 | }
65 |
66 | if dirty == "true" {
67 | return fmt.Sprintf("%s %s", banner, version)
68 | }
69 |
70 | return fmt.Sprintf("%s %s-%s", banner, version, commit)
71 | }
72 |
73 | func longVersion() string { //nolint:cyclop
74 | fallback := version
75 | if fallback == "" {
76 | fallback = "(unknown version)"
77 | }
78 |
79 | banner := banner
80 | if version != "" {
81 | banner = fmt.Sprintf("%s %s", banner, version)
82 | }
83 |
84 | readBuildSetting := buildSettingReader()
85 |
86 | version = "built"
87 |
88 | cgo, ok := readBuildSetting("CGO_ENABLED")
89 | if ok {
90 | if cgo == "1" {
91 | version += " with CGO"
92 | } else {
93 | version += " without CGO"
94 | }
95 | }
96 |
97 | vcs, ok := readBuildSetting("vcs")
98 | if !ok {
99 | return fmt.Sprintf("%s %s", banner, fallback)
100 | }
101 |
102 | version = fmt.Sprintf("%s from %s", version, vcs)
103 |
104 | commit, ok := readBuildSetting("vcs.revision")
105 | if !ok {
106 | return fmt.Sprintf("%s %s", banner, fallback)
107 | }
108 |
109 | version = fmt.Sprintf("%s commit %s", version, commit)
110 |
111 | timeStamp, ok := readBuildSetting("vcs.time")
112 | if ok {
113 | t, err := time.Parse(time.RFC3339, timeStamp)
114 | if err == nil {
115 | version = fmt.Sprintf("%s#%v", version, t.Format("2006-01-02"))
116 | }
117 | }
118 |
119 | dirty, ok := readBuildSetting("vcs.modified")
120 | if ok && dirty == "true" {
121 | version = fmt.Sprintf("%s (dirty)", version)
122 | }
123 |
124 | return fmt.Sprintf("%s %s", banner, version)
125 | }
126 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters:
2 | enable:
3 | - asciicheck
4 | - bidichk
5 | - bodyclose
6 | - containedctx
7 | - cyclop
8 | - decorder
9 | - dogsled
10 | - dupl
11 | - durationcheck
12 | - errcheck
13 | - errchkjson
14 | - errname
15 | - errorlint
16 | - exhaustive
17 | - exportloopref
18 | - forbidigo
19 | - forcetypeassert
20 | - gci
21 | - gochecknoinits
22 | - goconst
23 | - gocritic
24 | - gocyclo
25 | - godot
26 | - godox
27 | - gofmt
28 | - gofumpt
29 | - goheader
30 | - goimports
31 | - gomnd
32 | - gomodguard
33 | - goprintffuncname
34 | - gosec
35 | - gosimple
36 | - govet
37 | - grouper
38 | - importas
39 | - ineffassign
40 | - ireturn
41 | - lll
42 | - maintidx
43 | - makezero
44 | - misspell
45 | - nakedret
46 | - nestif
47 | - nilerr
48 | - nilnil
49 | - nlreturn
50 | - noctx
51 | - nolintlint
52 | - nosprintfhostport
53 | - prealloc
54 | - predeclared
55 | - promlinter
56 | - revive
57 | - sqlclosecheck
58 | - staticcheck
59 | - stylecheck
60 | - tagliatelle
61 | - tenv
62 | - thelper
63 | - tparallel
64 | - typecheck
65 | - unconvert
66 | - unparam
67 | - unused
68 | - wastedassign
69 | - whitespace
70 | - wsl
71 | disable:
72 | - golint # deprecated
73 | - interfacer # deprecated
74 | - scopelint # deprecated
75 | - structcheck # deprecated
76 | - deadcode # deprecated
77 | - varcheck # deprecated
78 | - gocognit # detects complex functions
79 | - maligned # checks if structs can be reordered for more efficient packing
80 | - rowserrcheck # checks if errors in DB queries are checked
81 | - funlen # detects long functions
82 | - depguard # checks imports against an allow-list
83 | - goerr113 # forbids dynamic errors like ad-hoc wrapping with fmt.Errorf
84 | - exhaustivestruct # requires struct initializations to contain all fields
85 | - testpackage # requires tests to be in a separate package
86 | - gochecknoglobals # forbids global variables
87 | - wrapcheck # requires errors from external packages to be wrapped
88 | - paralleltest # requires all test cases to run t.Parallel()
89 | - ifshort # requires expressions to be pulled into if statements if the result is only used there
90 | - gomoddirectives # forbids replacements in go.mod
91 | - varnamelen # forbids short variable names in longer scopes
92 | - contextcheck # checks if functions use a non-inherited context
93 | - execinquery # checks SQL queries
94 | - exhaustruct # checks if all structure fields are initialized
95 | - nonamedreturns # forbids named returns
96 | linters-settings:
97 | godox:
98 | keywords:
99 | - FIXME # FIXME generates a linter warning
100 | goconst:
101 | min-occurrences: 5
102 | tagliatelle:
103 | # check the struck tag name case
104 | case:
105 | rules:
106 | json: snake
107 | yaml: snake
108 | exhaustive:
109 | default-signifies-exhaustive: true
110 | gomnd:
111 | settings:
112 | mnd:
113 | ignored-numbers: 0o400,0o600,0o660,0o640,0o644,0o700,0o750
114 | ignored-functions: os.WriteFile,os.MkdirAll
115 | issues:
116 | exclude-use-default: false
117 |
--------------------------------------------------------------------------------
/hostinfo/arp_test.go:
--------------------------------------------------------------------------------
1 | package hostinfo
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | type macIPTestCase struct {
8 | ip string
9 | mac string
10 | }
11 |
12 | func TestGetMACFromLinuxArpContent(t *testing.T) {
13 | testGetMac(t, "testdata/linux_proc_net_arp", getMACFromLinuxARPContent, []macIPTestCase{
14 | {ip: "", mac: ""},
15 | {ip: "127.0.0.1", mac: ""},
16 | {ip: "192.168.56.101", mac: "08:00:27:56:c2:4c"},
17 | {ip: "10.0.2.3", mac: "52:54:00:12:35:03"},
18 | {ip: "192.168.56.100", mac: "0a:00:27:00:00:00"},
19 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
20 | })
21 |
22 | testGetMac(t, "testdata/unified/linux_proc_net_arp", getMACFromLinuxARPContent, []macIPTestCase{
23 | {ip: "", mac: ""},
24 | {ip: "127.0.0.1", mac: ""},
25 | {ip: "192.168.56.101", mac: "08:00:27:56:c2:4c"},
26 | {ip: "192.168.56.2", mac: "08:00:27:c2:e0:27"},
27 | {ip: "10.0.2.3", mac: "52:54:00:12:35:03"},
28 | {ip: "192.168.56.100", mac: "0a:00:27:00:00:00"},
29 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
30 | })
31 | }
32 |
33 | func TestGetIPFromLinuxARPContent(t *testing.T) {
34 | testGetIP(t, "testdata/linux_proc_net_arp", getIPFromLinuxARPContent, []macIPTestCase{
35 | {ip: "", mac: ""},
36 | {ip: "", mac: "01:01:01:01:01:01"},
37 | {ip: "192.168.56.101", mac: "08:00:27:56:c2:4c"},
38 | {ip: "10.0.2.3", mac: "52:54:00:12:35:03"},
39 | {ip: "192.168.56.100", mac: "0a:00:27:00:00:00"},
40 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
41 | })
42 |
43 | testGetIP(t, "testdata/unified/linux_proc_net_arp", getIPFromLinuxARPContent, []macIPTestCase{
44 | {ip: "", mac: ""},
45 | {ip: "", mac: "01:01:01:01:01:01"},
46 | {ip: "192.168.56.101", mac: "08:00:27:56:c2:4c"},
47 | {ip: "192.168.56.2", mac: "08:00:27:c2:e0:27"},
48 | {ip: "10.0.2.3", mac: "52:54:00:12:35:03"},
49 | {ip: "192.168.56.100", mac: "0a:00:27:00:00:00"},
50 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
51 | })
52 | }
53 |
54 | func TestGetMACFromWindowsARPOutput(t *testing.T) {
55 | testGetMac(t, "testdata/windows_arp", getMACFromWindowsARPOutput, []macIPTestCase{
56 | {ip: "", mac: ""},
57 | {ip: "127.0.0.1", mac: ""},
58 | {ip: "10.0.2.15", mac: ""},
59 | {ip: "192.168.56.101", mac: ""},
60 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
61 | {ip: "10.0.2.255", mac: "ff:ff:ff:ff:ff:ff"},
62 | {ip: "192.168.56.9", mac: "08:00:27:7e:ca:64"},
63 | {ip: "239.255.255.250", mac: "01:00:5e:7f:ff:fa"},
64 | })
65 |
66 | testGetMac(t, "testdata/unified/windows_arp", getMACFromWindowsARPOutput, []macIPTestCase{
67 | {ip: "", mac: ""},
68 | {ip: "127.0.0.1", mac: ""},
69 | {ip: "10.0.2.15", mac: ""},
70 | {ip: "192.168.56.9", mac: ""},
71 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
72 | {ip: "10.0.2.3", mac: "52:54:00:12:35:03"},
73 | {ip: "192.168.56.101", mac: "08:00:27:56:c2:4c"},
74 | {ip: "192.168.56.2", mac: "08:00:27:c2:e0:27"},
75 | {ip: "192.168.56.100", mac: "0a:00:27:00:00:00"},
76 | })
77 | }
78 |
79 | func TestGetIPFromWindowsARPOutput(t *testing.T) {
80 | testGetIP(t, "testdata/windows_arp", getIPFromWindowsARPOutput, []macIPTestCase{
81 | {ip: "", mac: ""},
82 | {ip: "", mac: "01:01:01:01:01:01"},
83 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
84 | {ip: "10.0.2.255", mac: "ff:ff:ff:ff:ff:ff"},
85 | {ip: "192.168.56.9", mac: "08:00:27:7e:ca:64"},
86 | {ip: "239.255.255.250", mac: "01:00:5e:7f:ff:fa"},
87 | })
88 |
89 | testGetIP(t, "testdata/unified/windows_arp", getIPFromWindowsARPOutput, []macIPTestCase{
90 | {ip: "", mac: ""},
91 | {ip: "", mac: "01:01:01:01:01:01"},
92 | {ip: "10.0.2.2", mac: "52:54:00:12:35:02"},
93 | {ip: "10.0.2.3", mac: "52:54:00:12:35:03"},
94 | {ip: "192.168.56.101", mac: "08:00:27:56:c2:4c"},
95 | {ip: "192.168.56.2", mac: "08:00:27:c2:e0:27"},
96 | {ip: "192.168.56.100", mac: "0a:00:27:00:00:00"},
97 | })
98 | }
99 |
--------------------------------------------------------------------------------
/hostinfo/generate_mac_vendors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import csv
4 | from dataclasses import dataclass
5 | import datetime
6 | import os.path
7 | import re
8 | import sys
9 | from typing import Iterator, Sequence
10 |
11 | import requests
12 |
13 |
14 | OUTPUT_FILE = "mac-vendors.txt"
15 |
16 | SOURCES = [
17 | "https://standards-oui.ieee.org/cid/cid.csv",
18 | "https://standards-oui.ieee.org/iab/iab.csv",
19 | "https://standards-oui.ieee.org/oui/oui.csv",
20 | "https://standards-oui.ieee.org/oui28/mam.csv",
21 | "https://standards-oui.ieee.org/oui36/oui36.csv",
22 | ]
23 |
24 | MAC_PREFIX_FIELD = 1
25 | ORG_FIELD = 2
26 |
27 | MAX_ORG_SIZE = 12
28 | IGNORED_TERMS = [
29 | "gmbh",
30 | "inc",
31 | "llc",
32 | "a/s",
33 | "ag",
34 | "s.r.l",
35 | "ltd",
36 | "sa",
37 | "s.l",
38 | "co",
39 | "trading",
40 | "limited",
41 | "incorporated",
42 | "corporate",
43 | "corporation",
44 | "technologies",
45 | "technology",
46 | ]
47 |
48 |
49 | @dataclass
50 | class MacVendorEntry:
51 | mac_prefix: str
52 | vendor_short: str
53 | vendor: str
54 |
55 | @property
56 | def csv_line(self) -> tuple[str, str, str]:
57 | return (self.mac_prefix, self.vendor_short, self.vendor)
58 |
59 |
60 | def all_mac_vendor_entries(urls: Sequence[str]) -> Iterator[MacVendorEntry]:
61 | for url in urls:
62 | print(f"Downloading {url}", file=sys.stderr)
63 | csv_data = download(url)
64 |
65 | print(" Processing...", file=sys.stderr)
66 | yield from mac_vendor_entries_from_csv(csv_data)
67 |
68 |
69 | def mac_vendor_entries_from_csv(csv_content: str) -> Iterator[MacVendorEntry]:
70 | csv_reader = csv.reader(csv_content.splitlines(), delimiter=",", quotechar='"')
71 | next(csv_reader, None) # skip headers
72 |
73 | for row in csv_reader:
74 | mac = process_mac_prefix(row[MAC_PREFIX_FIELD])
75 | short_org = shorten_org(row[ORG_FIELD])
76 |
77 | yield MacVendorEntry(
78 | mac_prefix=mac, vendor_short=short_org, vendor=row[ORG_FIELD]
79 | )
80 |
81 |
82 | def process_mac_prefix(raw_mac: str) -> str:
83 | return ":".join([raw_mac[i : i + 2].upper() for i in range(0, len(raw_mac), 2)])
84 |
85 |
86 | def shorten_org(org: str) -> str:
87 | org = org.strip()
88 |
89 | for term in IGNORED_TERMS:
90 | org = re.sub(
91 | f"[\\s\\W]({re.escape(term)})(?:[\\s\\W]|\\Z)", "", org, flags=re.IGNORECASE
92 | )
93 |
94 | return re.sub("[\\s\\W]", "", org)[:MAX_ORG_SIZE]
95 |
96 |
97 | def download(url: str) -> str:
98 | res = requests.get(url)
99 | res.raise_for_status()
100 |
101 | return res.text
102 |
103 |
104 | def main():
105 | mac_prefixes: set[str] = set()
106 |
107 | with open(OUTPUT_FILE, "w") as mac_file:
108 | mac_file.write(
109 | f"# The file was automatically generated "
110 | f"on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} "
111 | f"by {os.path.basename(__file__)} via `go generate`\n"
112 | )
113 |
114 | csv_writer = csv.writer(
115 | mac_file, delimiter="\t", quotechar='"', quoting=csv.QUOTE_MINIMAL
116 | )
117 |
118 | for mac_vendor_entry in all_mac_vendor_entries(SOURCES):
119 | if mac_vendor_entry.mac_prefix in mac_prefixes:
120 | print(
121 | f" Ignore duplicate entry: {mac_vendor_entry.mac_prefix} "
122 | f"({mac_vendor_entry.vendor})"
123 | )
124 | continue
125 |
126 | mac_prefixes.add(mac_vendor_entry.mac_prefix)
127 | csv_writer.writerow(mac_vendor_entry.csv_line)
128 |
129 | print(f"Created {OUTPUT_FILE}", file=sys.stderr)
130 |
131 |
132 | if __name__ == "__main__":
133 | main()
134 |
--------------------------------------------------------------------------------
/dhcpv6_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "math/rand"
7 | "net"
8 | "testing"
9 |
10 | "github.com/insomniacslk/dhcp/dhcpv6"
11 | )
12 |
13 | func TestDHCPv6SolicitAdvertise(t *testing.T) {
14 | testDHCPv6Response(t, "testdata/dhcpv6_solicit.bin", "testdata/dhcpv6_advertise.bin")
15 | }
16 |
17 | func TestDHCPv6SolicitRequestReply(t *testing.T) {
18 | testDHCPv6Response(t, "testdata/dhcpv6_request.bin", "testdata/dhcpv6_request_reply.bin")
19 | }
20 |
21 | func TestDHCPv6SolicitRenewReply(t *testing.T) {
22 | testDHCPv6Response(t, "testdata/dhcpv6_renew.bin", "testdata/dhcpv6_renew_reply.bin")
23 | }
24 |
25 | func TestGenerateDeterministicRandomAddress(t *testing.T) {
26 | sampleSize := 500
27 |
28 | t.Run("same_input", func(t *testing.T) {
29 | inputIP := mustParseIP(t, "fe80::9c90:a097:867d:e039")
30 | reference, err := generateDeterministicRandomAddress(inputIP)
31 | if err != nil {
32 | t.Fatalf("generate deterministic random address: %v", err)
33 | }
34 |
35 | assertLinkLocalIPv6(t, reference)
36 |
37 | for i := 0; i < sampleSize; i++ {
38 | ip, err := generateDeterministicRandomAddress(inputIP)
39 | if err != nil {
40 | t.Fatalf("generate deterministic random address: %v", err)
41 | }
42 |
43 | assertLinkLocalIPv6(t, ip)
44 |
45 | if !ip.Equal(reference) {
46 | t.Errorf("a different address was generated in iteration %d", i)
47 | }
48 | }
49 | })
50 |
51 | t.Run("different_input", func(t *testing.T) {
52 | seen := map[string]bool{}
53 |
54 | for i := 0; i < sampleSize; i++ {
55 | randomPart := make([]byte, net.IPv6len/2)
56 |
57 | binary.LittleEndian.PutUint64(randomPart, rand.Uint64()) //nolint:gosec
58 |
59 | inputIP := append([]byte{}, dhcpv6LinkLocalPrefix...)
60 | inputIP = append(inputIP, randomPart...)
61 |
62 | ip, err := generateDeterministicRandomAddress(net.IP(inputIP))
63 | if err != nil {
64 | t.Fatalf("generate deterministic random address: %v", err)
65 | }
66 |
67 | assertLinkLocalIPv6(t, ip)
68 |
69 | if seen[ip.String()] {
70 | t.Errorf("a dublicate address was generated in iteration %d", i)
71 | }
72 |
73 | seen[ip.String()] = true
74 | }
75 | })
76 | }
77 |
78 | func TestNewPeerInfo(t *testing.T) {
79 | solicit, err := readDHCPv6Message(t, "testdata/dhcpv6_solicit.bin").GetInnerMessage()
80 | if err != nil {
81 | t.Fatalf("get inner message: %v", err)
82 | }
83 |
84 | sourceAddr := &net.UDPAddr{IP: mustParseIP(t, "fe80::d422:2ab:8bf4:7381"), Port: 1234}
85 | expectedHostname := "win10vm"
86 |
87 | peerInfo := newPeerInfo(sourceAddr, solicit)
88 |
89 | if !peerInfo.IP.Equal(sourceAddr.IP) {
90 | t.Errorf("peer info contains IP %s instead of %s", peerInfo.IP, sourceAddr.IP)
91 | }
92 |
93 | if len(peerInfo.Hostnames) != 1 {
94 | t.Fatalf("peer info contains %d hostnames instead of one", len(peerInfo.Hostnames))
95 | }
96 |
97 | if peerInfo.Hostnames[0] != expectedHostname {
98 | t.Errorf("hostname is %q instead of %q", peerInfo.Hostnames[0], expectedHostname)
99 | }
100 | }
101 |
102 | func testDHCPv6Response(tb testing.TB, requestFileName string, responseFileName string) {
103 | tb.Helper()
104 |
105 | clientAddr := &net.UDPAddr{IP: mustParseIP(tb, "fe80::2"), Port: 1234}
106 | solicit := readDHCPv6Message(tb, requestFileName)
107 | config := Config{
108 | Interface: &net.Interface{HardwareAddr: mustParseMAC(tb, "08:00:27:7e:ca:64")},
109 | LeaseLifetime: dhcpv6DefaultValidLifetime,
110 | LocalIPv6: mustParseIP(tb, "fe80::a00:27ff:fe7e:ca64"),
111 | }
112 |
113 | advertise, err := NewDHCPv6Handler(config, nil).createResponse(clientAddr, solicit)
114 | if err != nil {
115 | tb.Fatalf("create response: %v", err)
116 | }
117 |
118 | expectedAdvertise := readFile(tb, responseFileName)
119 |
120 | if !bytes.Equal(advertise.ToBytes(), expectedAdvertise) {
121 | tb.Fatalf("advertise bytes do not match")
122 | }
123 | }
124 |
125 | func readDHCPv6Message(tb testing.TB, fileName string) *dhcpv6.Message {
126 | tb.Helper()
127 |
128 | msg, err := dhcpv6.MessageFromBytes(readFile(tb, fileName))
129 | if err != nil {
130 | tb.Fatalf("read DHCPv6 message from bytes: %v", err)
131 | }
132 |
133 | return msg
134 | }
135 |
136 | func mustParseMAC(tb testing.TB, mac string) net.HardwareAddr {
137 | tb.Helper()
138 |
139 | hwa, err := net.ParseMAC(mac)
140 | if err != nil {
141 | tb.Fatalf("parse MAC: %v", err)
142 | }
143 |
144 | return hwa
145 | }
146 |
147 | func assertLinkLocalIPv6(tb testing.TB, ip net.IP) {
148 | tb.Helper()
149 |
150 | if ip.To4() != nil {
151 | tb.Fatalf("IP %s is an IPv4 address instead of an IPv6 address", ip)
152 | }
153 |
154 | if len(ip) != net.IPv6len {
155 | tb.Fatalf("IP %s contains %d bytes instead of %d bytes (IPv6)",
156 | ip, len(ip), net.IPv6len)
157 | }
158 |
159 | if !bytes.Equal(ip[:net.IPv6len/2], dhcpv6LinkLocalPrefix) {
160 | tb.Fatalf("IP does not have link local prefix: %s", ip)
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/defaults.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os"
7 | "strings"
8 | "time"
9 | )
10 |
11 | const fallbackLogFileEnvironmentVariable = "PRETENDER_LOG_FILE"
12 |
13 | var (
14 | // vendors can set the following values to tweak the default configuration
15 | // during compilation as with -ldflags "-X main.vendorInterface=eth1".
16 |
17 | vendorInterface = ""
18 | vendorRelayIPv4 = ""
19 | vendorRelayIPv6 = ""
20 | vendorSOAHostname = ""
21 |
22 | vendorNoDHCPv6DNSTakeover = ""
23 | vendorNoDHCPv6 = ""
24 | vendorNoDNS = ""
25 | vendorNoRA = ""
26 | vendorNoMDNS = ""
27 | vendorNoNetBIOS = ""
28 | vendorNoLLMNR = ""
29 | vendorNoLocalNameResolution = ""
30 | vendorNoIPv6LNR = ""
31 |
32 | vendorSpoof = ""
33 | vendorDontSpoof = ""
34 | vendorSpoofFor = ""
35 | vendorDontSpoofFor = ""
36 | vendorSpoofTypes = ""
37 | vendorIgnoreDHCPv6NoFQDN = ""
38 | vendorDryMode = ""
39 |
40 | vendorTTL = ""
41 | vendorLeaseLifetime = ""
42 | vendorRARouterLifetime = ""
43 | vendorRAPeriod = ""
44 |
45 | vendorStopAfter = ""
46 | vendorVerbose = ""
47 | vendorNoColor = ""
48 | vendorNoTimestamps = ""
49 | vendorLogFileName = ""
50 | vendorNoHostInfo = ""
51 | vendorHideIgnored = ""
52 | vendorRedirectStderr = ""
53 | vendorListInterfaces = ""
54 | )
55 |
56 | var (
57 | defaultInterface = vendorInterface
58 | defaultRelayIPv4 = forceIP(vendorRelayIPv4, nil)
59 | defaultRelayIPv6 = forceIP(vendorRelayIPv6, nil)
60 | defaultSOAHostname = vendorSOAHostname
61 |
62 | defaultNoDHCPv6DNSTakeover = forceBool(vendorNoDHCPv6DNSTakeover, false)
63 | defaultNoDHCPv6 = forceBool(vendorNoDHCPv6, false)
64 | defaultNoDNS = forceBool(vendorNoDNS, false)
65 | defaultNoRA = forceBool(vendorNoRA, false)
66 | defaultNoMDNS = forceBool(vendorNoMDNS, false)
67 | defaultNoNetBIOS = forceBool(vendorNoNetBIOS, false)
68 | defaultNoLLMNR = forceBool(vendorNoLLMNR, false)
69 | defaultNoLocalNameResolution = forceBool(vendorNoLocalNameResolution, false)
70 | defaultNoIPv6LNR = forceBool(vendorNoIPv6LNR, false)
71 |
72 | defaultSpoof = forceStrings(vendorSpoof)
73 | defaultDontSpoof = forceStrings(vendorDontSpoof)
74 | defaultSpoofFor = forceStrings(vendorSpoofFor)
75 | defaultDontSpoofFor = forceStrings(vendorDontSpoofFor)
76 | defaultSpoofTypes = forceStrings(vendorSpoofTypes)
77 | defaultIgnoreDHCPv6NoFQDN = forceBool(vendorIgnoreDHCPv6NoFQDN, false)
78 | defaultDryMode = forceBool(vendorDryMode, false)
79 |
80 | defaultTTL = forceDuration(vendorTTL, dnsDefaultTTL)
81 | defaultLeaseLifetime = forceDuration(vendorLeaseLifetime, dhcpv6DefaultValidLifetime)
82 | defaultRARouterLifetime = forceDuration(vendorRARouterLifetime, raDefaultRouterLifetime)
83 | defaultRAPeriod = forceDuration(vendorRAPeriod, raDefaultPeriod)
84 |
85 | defaultStopAfter = forceDuration(vendorStopAfter, 0)
86 | defaultVerbose = forceBool(vendorVerbose, false)
87 | defaultNoColor = forceBool(vendorNoColor, false)
88 | defaultNoTimestamps = forceBool(vendorNoTimestamps, false)
89 | defaultHideIgnored = forceBool(vendorHideIgnored, false)
90 | defaultLogFileName = fromEnvironmentIfEmpty(vendorLogFileName, fallbackLogFileEnvironmentVariable)
91 | defaultNoHostInfo = forceBool(vendorNoHostInfo, false)
92 | defaultRedirectStderr = forceBool(vendorRedirectStderr, false)
93 | defaultListInterfaces = forceBool(vendorListInterfaces, false)
94 | )
95 |
96 | func forceIP(ipString string, fallbackIP net.IP) net.IP {
97 | if ipString == "" {
98 | return fallbackIP
99 | }
100 |
101 | ip := net.ParseIP(ipString)
102 | if ip == nil {
103 | panic(fmt.Sprintf("cannot parse IP %q", ipString))
104 | }
105 |
106 | return ip
107 | }
108 |
109 | func forceStrings(input string) []string {
110 | if input == "" {
111 | return nil
112 | }
113 |
114 | res := make([]string, 0, len(input))
115 |
116 | for _, s := range strings.Split(input, ",") {
117 | res = append(res, strings.TrimSpace(s))
118 | }
119 |
120 | return res
121 | }
122 |
123 | func forceBool(boolString string, fallbackBool bool) bool { //nolint:unparam
124 | switch strings.ToLower(boolString) {
125 | case "":
126 | return fallbackBool
127 | case "true", "1", "yes":
128 | return true
129 | case "false", "0", "no":
130 | return false
131 | default:
132 | panic(fmt.Sprintf("cannot parse bool %q", boolString))
133 | }
134 | }
135 |
136 | func forceDuration(durationString string, fallbackDuration time.Duration) time.Duration {
137 | if durationString == "" {
138 | return fallbackDuration
139 | }
140 |
141 | d, err := time.ParseDuration(durationString)
142 | if err != nil {
143 | panic(fmt.Sprintf("cannot parse duration %q", durationString))
144 | }
145 |
146 | return d
147 | }
148 |
149 | func fromEnvironmentIfEmpty(primaryValue string, fallbackEnvVariable string) string {
150 | if primaryValue != "" {
151 | return primaryValue
152 | }
153 |
154 | return os.Getenv(fallbackEnvVariable)
155 | }
156 |
--------------------------------------------------------------------------------
/hostinfo/arp.go:
--------------------------------------------------------------------------------
1 | package hostinfo
2 |
3 | import (
4 | "net"
5 | "runtime"
6 | "strings"
7 | )
8 |
9 | var (
10 | linuxArpPseudoFile = "/proc/net/arp"
11 | windowsArpCommandAvailable = commandAvailable("arp", osWindows)
12 | linuxArpPseudoFileAvailable = fileAvailable(linuxArpPseudoFile, osLinux)
13 | )
14 |
15 | func getMACFromLinuxARP(ip net.IP) net.HardwareAddr {
16 | if !linuxArpPseudoFileAvailable || runtime.GOOS != osLinux {
17 | return nil
18 | }
19 |
20 | if testMode {
21 | return getMACFromLinuxARPContent(ip, readFileIfPossible("testdata/unified/linux_proc_net_arp"))
22 | }
23 |
24 | return getMACFromLinuxARPContent(ip, readFileIfPossible(linuxArpPseudoFile))
25 | }
26 |
27 | func getMACFromLinuxARPContent(ip net.IP, arpContent []byte) net.HardwareAddr {
28 | if ip == nil || arpContent == nil {
29 | return nil
30 | }
31 |
32 | for _, line := range strings.Split(string(arpContent), "\n") {
33 | parts := strings.Fields(line)
34 | if len(parts) < 6 { //nolint:gomnd
35 | continue
36 | }
37 |
38 | if ip.Equal(net.ParseIP(parts[0])) {
39 | mac, err := net.ParseMAC(parts[3])
40 | if err == nil {
41 | return mac
42 | }
43 | }
44 | }
45 |
46 | return nil
47 | }
48 |
49 | func getIPFromLinuxARP(mac net.HardwareAddr) net.IP {
50 | if !linuxArpPseudoFileAvailable || runtime.GOOS != osLinux {
51 | return nil
52 | }
53 |
54 | if testMode {
55 | return getIPFromLinuxARPContent(mac, readFileIfPossible("testdata/unified/linux_proc_net_arp"))
56 | }
57 |
58 | return getIPFromLinuxARPContent(mac, readFileIfPossible(linuxArpPseudoFile))
59 | }
60 |
61 | func getIPFromLinuxARPContent(mac net.HardwareAddr, arpContent []byte) net.IP {
62 | if mac == nil || arpContent == nil {
63 | return nil
64 | }
65 |
66 | for _, line := range strings.Split(string(arpContent), "\n") {
67 | parts := strings.Fields(line)
68 | if len(parts) < 6 { //nolint:gomnd
69 | continue
70 | }
71 |
72 | if strings.EqualFold(mac.String(), parts[3]) {
73 | ip := net.ParseIP(parts[0])
74 | if ip != nil {
75 | return ip
76 | }
77 | }
78 | }
79 |
80 | return nil
81 | }
82 |
83 | func getMACFromWindowsARP(ip net.IP) net.HardwareAddr {
84 | if !windowsArpCommandAvailable || runtime.GOOS != osWindows {
85 | return nil
86 | }
87 |
88 | if testMode {
89 | return getMACFromWindowsARPOutput(ip, readFileIfPossible("testdata/unified/windows_arp"))
90 | }
91 |
92 | return getMACFromWindowsARPOutput(ip, readOutput(execTimeoutWindows,
93 | "arp", "-a"))
94 | }
95 |
96 | //nolint:gomnd
97 | func getMACFromWindowsARPOutput(ip net.IP, arpOutput []byte) net.HardwareAddr {
98 | if ip == nil || arpOutput == nil {
99 | return nil
100 | }
101 |
102 | skip := false
103 |
104 | for _, line := range strings.Split(string(arpOutput), "\n") {
105 | if len(line) == 0 {
106 | continue
107 | }
108 |
109 | // interface section header
110 | if line[0] != ' ' {
111 | // skip next line because it's a table header
112 | skip = true
113 |
114 | continue
115 | }
116 |
117 | if skip {
118 | // skip table header
119 | skip = false
120 |
121 | continue
122 | }
123 |
124 | fields := strings.Fields(line)
125 | if len(fields) < 2 {
126 | continue
127 | }
128 |
129 | lineIP := net.ParseIP(fields[0])
130 | if !ip.Equal(lineIP) {
131 | continue
132 | }
133 |
134 | mac, err := net.ParseMAC(fields[1])
135 | if err == nil {
136 | return mac
137 | }
138 | }
139 |
140 | return nil
141 | }
142 |
143 | func getIPFromWindowsARP(mac net.HardwareAddr) net.IP {
144 | if !windowsArpCommandAvailable || runtime.GOOS != osWindows {
145 | return nil
146 | }
147 |
148 | if testMode {
149 | return getIPFromWindowsARPOutput(mac, readFileIfPossible("testdata/unified/windows_arp"))
150 | }
151 |
152 | return getIPFromWindowsARPOutput(mac, readOutput(execTimeoutWindows,
153 | "arp", "-a"))
154 | }
155 |
156 | func getIPFromWindowsARPOutput(mac net.HardwareAddr, arpOutput []byte) net.IP {
157 | skip := false
158 |
159 | for _, line := range strings.Split(string(arpOutput), "\n") {
160 | if len(line) == 0 {
161 | continue
162 | }
163 |
164 | // interface section header
165 | if line[0] != ' ' {
166 | // skip next line because it's a table header
167 | skip = true
168 |
169 | continue
170 | }
171 |
172 | if skip {
173 | // skip table header
174 | skip = false
175 |
176 | continue
177 | }
178 |
179 | fields := strings.Fields(line)
180 | if len(fields) < 2 { //nolint:gomnd
181 | continue
182 | }
183 |
184 | lineMAC, err := net.ParseMAC(strings.ReplaceAll(fields[1], "-", ":"))
185 | if err != nil || !strings.EqualFold(lineMAC.String(), mac.String()) {
186 | continue
187 | }
188 |
189 | ip := net.ParseIP(fields[0])
190 | if ip != nil {
191 | return ip
192 | }
193 | }
194 |
195 | return nil
196 | }
197 |
198 | func getIPFromARP(mac net.HardwareAddr) net.IP {
199 | if runtime.GOOS == osWindows {
200 | return getIPFromWindowsARP(mac)
201 | }
202 |
203 | return getIPFromLinuxARP(mac)
204 | }
205 |
206 | func getMACFromOS(ip net.IP) net.HardwareAddr {
207 | switch runtime.GOOS {
208 | case osLinux:
209 | if ip.To4() == nil {
210 | return getMACFromLinuxIPNeighbor(ip)
211 | }
212 |
213 | return getMACFromLinuxARP(ip)
214 | case osWindows:
215 | if ip.To4() == nil {
216 | return getMACFromWindowsNetshShowNeighbors(ip)
217 | }
218 |
219 | return getMACFromWindowsARP(ip)
220 | default:
221 | return nil
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/local_name_resolution_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "net"
6 | "testing"
7 | "time"
8 |
9 | "github.com/miekg/dns"
10 | )
11 |
12 | func TestLLMNR(t *testing.T) {
13 | testReply(t, "testdata/llmnr_request.bin", "testdata/llmnr_reply.bin")
14 | }
15 |
16 | func TestMDNS(t *testing.T) {
17 | testReply(t, "testdata/mdns_request.bin", "testdata/mdns_reply.bin")
18 | }
19 |
20 | func TestNetBIOS(t *testing.T) { //nolint:cyclop
21 | relayIP := mustParseIP(t, "10.0.0.2")
22 | mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(t, "10.0.0.1")}}
23 | request := readNameServiceMessage(t, "testdata/netbios_request.bin")
24 | expectedReply := readFile(t, "testdata/netbios_reply.bin")
25 | cfg := Config{RelayIPv4: relayIP, TTL: 60 * time.Second}
26 |
27 | reply := createDNSReplyFromRequest(mockRW, request, nil, cfg)
28 | if reply == nil {
29 | t.Fatalf("no message was created")
30 | }
31 |
32 | if !reply.Authoritative {
33 | t.Fatalf("reply is not authoritative such that it will not be considered by Windows")
34 | }
35 |
36 | if len(reply.Answer) != 1 {
37 | t.Fatalf("received %d answers instead of 1", len(reply.Answer))
38 | }
39 |
40 | answer, ok := reply.Answer[0].(*dns.NIMLOC)
41 | if !ok {
42 | t.Fatalf("wrong answer type: %T", reply.Answer[0])
43 | }
44 |
45 | expectedLocator := encodeNetBIOSLocator(relayIP)
46 | if answer.Locator != expectedLocator {
47 | t.Fatalf("unexpected locator: got %q instead of %q", answer.Locator, expectedLocator)
48 | }
49 |
50 | if answer.Hdr.Ttl != uint32(cfg.TTL.Seconds()) {
51 | t.Fatalf("unexpected TTL: got %ds instead of %ds", answer.Hdr.Ttl, uint32(cfg.TTL.Seconds()))
52 | }
53 |
54 | if answer.Hdr.Name != request.Question[0].Name {
55 | t.Fatalf("%q was echoed instead of question name %q", answer.Hdr.Name, reply.Question[0].Name)
56 | }
57 |
58 | if answer.Hdr.Rrtype != typeNetBios {
59 | t.Fatalf("unexpected type: got %s instead of NIMLOC/NetBIOS", dnsQueryType(answer.Hdr.Rrtype))
60 | }
61 |
62 | if answer.Hdr.Class != dns.ClassINET {
63 | t.Fatalf("unexpected class: got %d instead of %d", answer.Hdr.Class, dns.ClassINET)
64 | }
65 |
66 | if reply.CheckingDisabled {
67 | t.Fatalf("checking disabled should be false in NetBIOS reply")
68 | }
69 |
70 | if reply.Question != nil {
71 | t.Fatalf("NetBIOS reply cannot include question")
72 | }
73 |
74 | replyBuffer, err := reply.Pack()
75 | if err != nil {
76 | t.Fatalf("pack reply: %v", err)
77 | }
78 |
79 | if !bytes.Equal(replyBuffer, expectedReply) {
80 | t.Fatalf("reply bytes do not match")
81 | }
82 | }
83 |
84 | func TestSubnetBroadcastListenIP(t *testing.T) {
85 | testCases := []struct {
86 | Net string
87 | BroadcastIP string
88 | }{
89 | {"192.168.0.4/24", "192.168.0.255"},
90 | {"10.0.0.10/23", "10.0.1.255"},
91 | {"10.0.0.0/8", "10.255.255.255"},
92 | {"192.168.5.16/30", "192.168.5.19"},
93 | }
94 |
95 | for _, testCase := range testCases {
96 | testCase := testCase
97 |
98 | t.Run(testCase.Net, func(t *testing.T) {
99 | _, ipNet, err := net.ParseCIDR(testCase.Net)
100 | if err != nil {
101 | t.Fatalf("invalid net %q: %v", testCase.Net, err)
102 | }
103 |
104 | expected := net.ParseIP(testCase.BroadcastIP)
105 | if expected == nil {
106 | t.Fatalf("invalid broadcast IP: %s", testCase.BroadcastIP)
107 | }
108 |
109 | broadcastIP, err := subnetBroadcastListenIP(ipNet)
110 | if err != nil {
111 | t.Fatalf("calculating broadcast listen IP: %v", err)
112 | }
113 |
114 | if !broadcastIP.Equal(expected) {
115 | t.Fatalf("expected broadcast IP %s for net %s, got %s instead",
116 | expected, ipNet, broadcastIP)
117 | }
118 | })
119 | }
120 | }
121 |
122 | func TestDecodeNetBIOSHostname(t *testing.T) {
123 | testCases := []struct {
124 | NetBIOSName string
125 | Expected string
126 | }{
127 | {NetBIOSName: "FEEFFDFEEIEPFDFECACACACACACACACA.", Expected: "TESTHOST"},
128 |
129 | // different NetBIOS Suffixes
130 | // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-brws/0c773bdd-78e2-4d8b-8b3d-b7506849847b
131 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACAAA.", Expected: "WORKGROUP"}, // workstation name
132 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACAAB.", Expected: "WORKGROUP"}, // messenger service
133 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACABL.", Expected: "WORKGROUP"}, // domain master browser
134 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACABN.", Expected: "WORKGROUP"}, // master browser
135 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACABO.", Expected: "WORKGROUP"}, // domain service elections
136 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACACA.", Expected: "WORKGROUP"}, // file service
137 | }
138 |
139 | for _, testCase := range testCases {
140 | testCase := testCase
141 |
142 | t.Run(testCase.Expected, func(t *testing.T) {
143 | decoded := decodeNetBIOSHostname(testCase.NetBIOSName)
144 | if decoded != testCase.Expected {
145 | t.Errorf("%s decoded to %s instead of %s",
146 | testCase.NetBIOSName, decoded, testCase.Expected)
147 | }
148 | })
149 | }
150 | }
151 |
152 | func TestDecodeNetBIOSSuffix(t *testing.T) {
153 | testCases := []struct {
154 | NetBIOSName string
155 | Expected string
156 | }{
157 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACAAA.", Expected: NetBIOSSuffixWorkstationService},
158 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACAAB.", Expected: NetBIOSSuffixWindowsMessengerService},
159 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACABL.", Expected: NetBIOSSuffixDomainMasterBrowser},
160 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACABN.", Expected: NetBIOSSuffixMasterBrowser},
161 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACABO.", Expected: NetBIOSSuffixBrowserServiceElections},
162 | {NetBIOSName: "FHEPFCELEHFCEPFFFACACACACACACACA.", Expected: NetBIOSSuffixFileService},
163 | }
164 |
165 | for _, testCase := range testCases {
166 | testCase := testCase
167 |
168 | t.Run(testCase.Expected, func(t *testing.T) {
169 | decoded := decodeNetBIOSSuffix(testCase.NetBIOSName)
170 | if decoded != testCase.Expected {
171 | t.Errorf("%s suffix decoded to %q instead of %q",
172 | testCase.NetBIOSName, decoded, testCase.Expected)
173 | }
174 | })
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/filter.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "strings"
7 |
8 | "github.com/miekg/dns"
9 | )
10 |
11 | const isatapHostname = "isatap"
12 |
13 | func containsDomain(haystack []string, needle string) bool {
14 | needle = strings.ToLower(strings.TrimSuffix(strings.TrimRight(needle, "."), ".local"))
15 |
16 | for _, el := range haystack {
17 | el := strings.ToLower(strings.TrimRight(el, "."))
18 |
19 | if strings.HasPrefix(el, ".") && strings.HasSuffix(needle, strings.TrimLeft(el, ".")) {
20 | return true
21 | } else if strings.EqualFold(el, needle) {
22 | return true
23 | }
24 | }
25 |
26 | return false
27 | }
28 |
29 | //nolint:cyclop
30 | func shouldRespondToNameResolutionQuery(config Config, host string, queryType uint16,
31 | from net.IP, fromHostnames []string,
32 | ) (bool, string) {
33 | if config.DryMode {
34 | return false, "dry mode"
35 | }
36 |
37 | if strings.HasPrefix(strings.ToLower(host), isatapHostname) {
38 | return false, "ISATAP is always ignored"
39 | }
40 |
41 | if len(config.SpoofFor) > 0 && !containsIP(config.SpoofFor, from) &&
42 | !containsAnyHostname(config.SpoofFor, fromHostnames) {
43 | return false, "host address and name not in spoof-for list"
44 | }
45 |
46 | if len(config.DontSpoofFor) > 0 {
47 | if containsIP(config.DontSpoofFor, from) {
48 | return false, "host address included in dont-spoof-for list"
49 | }
50 |
51 | if containsAnyHostname(config.DontSpoofFor, fromHostnames) {
52 | return false, "hostname included in dont-spoof-for list"
53 | }
54 | }
55 |
56 | if len(config.Spoof) > 0 && !containsDomain(config.Spoof, host) {
57 | return false, "domain not in spoof list"
58 | }
59 |
60 | if len(config.DontSpoof) > 0 && containsDomain(config.DontSpoof, host) {
61 | return false, "domain included in dont-spoof list"
62 | }
63 |
64 | if !config.SpoofTypes.ShouldSpoof(queryType) {
65 | return false, fmt.Sprintf("type %s is not in spoof-types", dnsQueryType(queryType))
66 | }
67 |
68 | switch {
69 | case queryType == dns.TypeA && config.RelayIPv4 == nil:
70 | return false, "no IPv4 relay address configured"
71 | case queryType == dns.TypeAAAA && config.RelayIPv6 == nil:
72 | return false, "no IPv6 relay address configured"
73 | }
74 |
75 | return true, ""
76 | }
77 |
78 | func shouldRespondToDHCP(config Config, from peerInfo) (bool, string) {
79 | if config.DryMode {
80 | return false, "dry mode"
81 | }
82 |
83 | if len(from.Hostnames) == 0 && config.IgnoreDHCPv6NoFQDN {
84 | return false, "no FQDN in DHCPv6 message"
85 | }
86 |
87 | if len(config.SpoofFor) > 0 && !containsPeer(config.SpoofFor, from) {
88 | return false, "host not in spoof-for list"
89 | }
90 |
91 | if len(config.DontSpoofFor) > 0 && containsPeer(config.DontSpoofFor, from) {
92 | return false, "host included in dont-spoof-for list"
93 | }
94 |
95 | return true, ""
96 | }
97 |
98 | type hostMatcher struct {
99 | IPs []net.IP
100 | Hostname string
101 | }
102 |
103 | var hostMatcherLookupFunction = net.LookupIP
104 |
105 | func newHostMatcher(hostnameOrIP string) *hostMatcher {
106 | ip := net.ParseIP(hostnameOrIP)
107 | if ip != nil { // hostnameOrIP is an IP
108 | return &hostMatcher{IPs: []net.IP{ip}}
109 | }
110 |
111 | // domain is a wildcard
112 | if strings.HasPrefix(hostnameOrIP, ".") {
113 | return &hostMatcher{Hostname: hostnameOrIP}
114 | }
115 |
116 | // hostnameOrIP is not an IP
117 | ips, _ := hostMatcherLookupFunction(hostnameOrIP)
118 |
119 | return &hostMatcher{
120 | IPs: ips,
121 | Hostname: hostnameOrIP,
122 | }
123 | }
124 |
125 | func asHostMatchers(hostnamesOrIPs []string) []*hostMatcher {
126 | hosts := make([]*hostMatcher, 0, len(hostnamesOrIPs))
127 |
128 | for _, hostnameOrIP := range hostnamesOrIPs {
129 | hosts = append(hosts, newHostMatcher(hostnameOrIP))
130 | }
131 |
132 | return hosts
133 | }
134 |
135 | func normalizeHostname(hostname string) string {
136 | return strings.ToLower(strings.TrimRight(hostname, "."))
137 | }
138 |
139 | // Matches determines whether or not the host matches any of the provided hostnames.
140 | func (h *hostMatcher) MatchesAnyHostname(hostnames ...string) bool {
141 | if h.Hostname == "" {
142 | return false
143 | }
144 |
145 | thisHostname := normalizeHostname(h.Hostname)
146 |
147 | for _, hostname := range hostnames {
148 | otherHostname := normalizeHostname(hostname)
149 |
150 | // hostname matches
151 | if thisHostname == otherHostname {
152 | return true
153 | }
154 |
155 | // subdomain matches
156 | if strings.HasPrefix(thisHostname, ".") && strings.HasSuffix(otherHostname, thisHostname) {
157 | return true
158 | }
159 | }
160 |
161 | return false
162 | }
163 |
164 | func (h *hostMatcher) String() string {
165 | if len(h.IPs) == 0 && h.Hostname == "" {
166 | return "no host"
167 | }
168 |
169 | ipStrings := make([]string, 0, len(h.IPs))
170 |
171 | for _, ip := range h.IPs {
172 | ipStrings = append(ipStrings, ip.String())
173 | }
174 |
175 | ipsString := strings.Join(ipStrings, ", ")
176 |
177 | if h.Hostname == "" {
178 | return ipsString
179 | }
180 |
181 | if len(h.IPs) == 0 {
182 | return h.Hostname
183 | }
184 |
185 | return fmt.Sprintf("%s (%s)", h.Hostname, ipsString)
186 | }
187 |
188 | type spoofTypes struct {
189 | A bool
190 | AAAA bool
191 | ANY bool
192 | SOA bool
193 | }
194 |
195 | func parseSpoofTypes(spoofTypesStrings []string) (*spoofTypes, error) {
196 | if len(spoofTypesStrings) == 0 {
197 | return nil, nil //nolint:nilnil
198 | }
199 |
200 | st := &spoofTypes{}
201 |
202 | for _, spoofType := range spoofTypesStrings {
203 | switch strings.ToLower(spoofType) {
204 | case "a":
205 | st.A = true
206 | case "aaaa":
207 | st.AAAA = true
208 | case "any":
209 | st.ANY = true
210 | case "soa":
211 | st.SOA = true
212 | default:
213 | return nil, fmt.Errorf("unknown query type: %s", spoofType)
214 | }
215 | }
216 |
217 | return st, nil
218 | }
219 |
220 | func (st *spoofTypes) ShouldSpoof(qType uint16) bool { //nolint:cyclop
221 | if st == nil {
222 | return true
223 | }
224 |
225 | switch {
226 | case qType == typeNetBios:
227 | return true
228 | case qType == dns.TypeA && st.A:
229 | return true
230 | case qType == dns.TypeAAAA && st.AAAA:
231 | return true
232 | case qType == dns.TypeANY && st.ANY:
233 | return true
234 | case qType == dns.TypeSOA && st.SOA:
235 | return true
236 | default:
237 | return false
238 | }
239 | }
240 |
241 | func containsPeer(hosts []*hostMatcher, peer peerInfo) bool {
242 | for _, host := range hosts {
243 | if host.MatchesAnyHostname(peer.Hostnames...) {
244 | return true
245 | }
246 |
247 | for _, ip := range host.IPs {
248 | if peer.IP.Equal(ip) {
249 | return true
250 | }
251 | }
252 | }
253 |
254 | return false
255 | }
256 |
257 | func containsIP(haystack []*hostMatcher, needle net.IP) bool {
258 | for _, el := range haystack {
259 | for _, ip := range el.IPs {
260 | if needle.Equal(ip) {
261 | return true
262 | }
263 | }
264 | }
265 |
266 | return false
267 | }
268 |
269 | func containsAnyHostname(haystack []*hostMatcher, needles []string) bool {
270 | for _, el := range haystack {
271 | if el.MatchesAnyHostname(needles...) {
272 | return true
273 | }
274 | }
275 |
276 | return false
277 | }
278 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
pretender
3 | Your MitM sidekick for relaying attacks featuring DHCPv6 DNS takeover
as well as mDNS, LLMNR and NetBIOS-NS spoofing
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ---
13 |
14 | `pretender` is a tool developed by RedTeam Pentesting to obtain
15 | machine-in-the-middle positions via spoofed local name resolution and DHCPv6 DNS
16 | takeover attacks. `pretender` primarily targets Windows hosts, as it is intended
17 | to be used for relaying attacks but can be deployed on Linux, Windows and all
18 | other platforms Go supports. Name resolution queries can be answered with
19 | arbitrary IPs for situations where the relaying tool runs on a different host
20 | than `pretender`. It is designed to work with tools such as
21 | [Impacket's](https://github.com/SecureAuthCorp/impacket) `ntlmrelayx.py` and
22 | [krbrelayx](https://github.com/dirkjanm/krbrelayx) that handle the incoming
23 | connections for relaying attacks or hash dumping.
24 |
25 | Read our [blog
26 | post](https://blog.redteam-pentesting.de/2022/introducing-pretender/) for more
27 | information about DHCPv6 DNS takeover, local name resolution spoofing and relay
28 | attacks.
29 |
30 | ---
31 |
32 | ## Usage
33 |
34 | To get a feel for the situation in the local network, `pretender` can be started
35 | in `--dry` mode where it only logs incoming queries and does not answer any of
36 | them:
37 |
38 | ```sh
39 | pretender -i eth0 --dry
40 | pretender -i eth0 --dry --no-ra # without router advertisements
41 | ```
42 |
43 | To perform local name resolution spoofing via mDNS, LLMNR and NetBIOS-NS as well
44 | as a DHCPv6 DNS takeover with router advertisements, simply run `pretender` like
45 | this:
46 |
47 | ```sh
48 | pretender -i eth0
49 | ```
50 |
51 | You can disable certain attacks with `--no-dhcp-dns` (disabled DHCPv6, DNS and
52 | router advertisements), `--no-lnr` (disabled mDNS, LLMNR and NetBIOS-NS),
53 | `--no-mdns`, `--no-llmnr`, `--no-netbios` and `--no-ra`.
54 |
55 | If `ntlmrelayx.py` runs on a different host (say `10.0.0.10`/`fe80::5`), run
56 | `pretender` like this:
57 |
58 | ```sh
59 | pretender -i eth0 -4 10.0.0.10 -6 fe80::5
60 | ```
61 |
62 | Pretender can be setup to only respond to queries for certain domains (or all
63 | _but_ certain domains) and it can perform the spoofing attacks only for certain
64 | hosts (or all _but_ certain hosts). Referencing hosts by hostname relies on the
65 | name resolution of the host that runs `pretender`. See the following example:
66 |
67 | ```sh
68 | pretender -i eth0 --spoof example.com --dont-spoof-for 10.0.0.3,host1.corp,fe80::f --ignore-nofqdn
69 | ```
70 |
71 | For more information, run `pretender --help`.
72 |
73 | ---
74 |
75 | ## Tips
76 |
77 | - Make sure to enable IPv6 support in `ntlmrelayx.py` with the `-6` flag
78 | - Pretender can be configured to stop after a certain time period for situations
79 | where it cannot be aborted manually (`--stop-after` and
80 | `main.vendorStopAfter`)
81 | - Host info lookup (which relies on the ARP table, IP neighbours and reverse
82 | lookups) can be disabled with `--no-host-info` or `main.vendorNoHostInfo`
83 | - If you are not sure which interface to choose (especially on Windows), list
84 | all interfaces with names and addresses using `--interfaces`
85 | - If you want to exclude hosts from local name resolution spoofing, make sure to
86 | also exclude their IPv6 addresses or use `--no-ipv6-lnr`/`main.vendorNoIPv6LNR`
87 | - DHCPv6 messages usually contain a FQDN option (which can also sometimes
88 | contain a hostname which is not a FQDN). This option is used to filter out
89 | messages by hostname (`--spoof-for`/`--dont-spoof-for`). You can decide what
90 | to do with DHCPv6 messages without FQDN option by setting or omitting
91 | `--ignore-nofqdn`
92 | - Depending on the build configuration, either the operating system resolver
93 | (`CGO_ENABLED=1`) or a Go implementation (`CGO_ENABLED=0`) is used. This can
94 | be important for host info collection because the OS resolver may support
95 | local name resolution and the Go implementation does not, unless a stub
96 | resolver is used.
97 | - The host info functionality is currently only available for Windows and Linux.
98 | - A custom MAC address vendor list can be compiled into the binary by replacing
99 | the default list `hostinfo/mac-vendors.txt`. Only lines with MAC prefixes in
100 | the following format are recognized: `FF:FF:FFVendorIDVendor` (the
101 | MAC prefix length can be arbitrary).
102 | - If you only want to perform Kerberos relaying you can specify `--no-lnr` and
103 | `--spoof-types SOA` to ignore any queries that are unrelated to the attack.
104 | - When conducting a Kerberos relay attack where `krbrelayx.py` runs on a
105 | different host than pretender (relay IPv4 address points to different host
106 | that runs `krbrelayx.py`), the host running `krbrelayx.py` will also need to
107 | run pretender in order to receive and deny the Dynamic Update query sent to
108 | the relay IPv4 address.
109 |
110 | ---
111 |
112 | ## Building and Vendoring
113 |
114 | Pretender can be build as follows:
115 |
116 | ```sh
117 | go build
118 | ```
119 |
120 | Pretender can also be compiled with pre-configured settings. For this, the
121 | `ldflags` have to be modified like this:
122 |
123 | ```sh
124 | -ldflags '-X main.vendorInterface=eth1'
125 | ```
126 |
127 | For example, Pretender can be built for Windows with a specific default
128 | interface, without colored output and with a relay IPv4 address configured:
129 |
130 | ```
131 | GOOS=windows go build -trimpath -ldflags '-X "main.vendorInterface=Ethernet 2" -X main.vendorNoColor=true -X main.vendorRelayIPv4=10.0.0.10'
132 | ```
133 |
134 | Full list of vendoring options (see `defaults.go` or `pretender --help` for
135 | detailed information):
136 |
137 | ```
138 | vendorInterface
139 | vendorRelayIPv4
140 | vendorRelayIPv6
141 | vendorSOAHostname
142 | vendorNoDHCPv6DNSTakeover
143 | vendorNoDHCPv6
144 | vendorNoDNS
145 | vendorNoMDNS
146 | vendorNoNetBIOS
147 | vendorNoLLMNR
148 | vendorNoLocalNameResolution
149 | vendorNoRA
150 | vendorNoIPv6LNR
151 | vendorSpoof
152 | vendorDontSpoof
153 | vendorSpoofFor
154 | vendorDontSpoofFor
155 | vendorSpoofTypes
156 | vendorIgnoreDHCPv6NoFQDN
157 | vendorDryMode
158 | vendorTTL
159 | vendorLeaseLifetime
160 | vendorRARouterLifetime
161 | vendorRAPeriod
162 | vendorStopAfter
163 | vendorVerbose
164 | vendorNoColor
165 | vendorNoTimestamps
166 | vendorLogFileName
167 | vendorNoHostInfo
168 | vendorHideIgnored
169 | vendorRedirectStderr
170 | vendorListInterfaces
171 | ```
172 |
--------------------------------------------------------------------------------
/hostinfo/host_info_test.go:
--------------------------------------------------------------------------------
1 | package hostinfo
2 |
3 | import (
4 | "net"
5 | "os"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | //nolint:gochecknoinits
11 | func init() {
12 | testMode = true
13 |
14 | vendorsContent, err := os.ReadFile("testdata/unified/mac-vendors.txt")
15 | if err != nil {
16 | panic(err)
17 | }
18 |
19 | macVendorsFile = string(vendorsContent)
20 | }
21 |
22 | func TestCacheHostInfos(t *testing.T) {
23 | c := NewCache()
24 |
25 | ipv6 := mustParseIP(t, "fe80::a00:27ff:fe56:c24c")
26 | c.resolvedHostnames[ipv6.String()] = nil // unresolvable
27 |
28 | ipv4 := mustParseIP(t, "192.168.56.101")
29 | c.resolvedHostnames[ipv6.String()] = nil // unresolvable
30 |
31 | c.toMAC(ipv4)
32 |
33 | c.SaveMACFromIP(ipv6, nil)
34 |
35 | c.AddHostnamesForIP(ipv4, []string{"foo"})
36 | c.AddHostnamesForIP(ipv6, []string{"bar"})
37 |
38 | assertContainsExactly(t, c.HostInfos(ipv6), "foo", "bar", ipv4.String())
39 |
40 | otherIPv6 := mustParseIP(t, "fe80::800:27ff:fe00:0")
41 | c.resolvedHostnames[otherIPv6.String()] = nil // unresolvable
42 |
43 | // this IPv4 has the same MAC as otherIPv6
44 | c.resolvedHostnames[mustParseIP(t, "192.168.56.100").String()] = nil // unresolvable
45 |
46 | assertContainsExactly(t, c.HostInfos(otherIPv6),
47 | "192.168.56.100", "test")
48 | }
49 |
50 | func TestCacheSaveMACFromIP(t *testing.T) {
51 | c := NewCache()
52 |
53 | ipv4 := mustParseIP(t, "192.168.56.101")
54 | c.toMAC(ipv4)
55 |
56 | // ipv6 contains the EUI-64 encoded MAC that matches the ipv4 address above
57 | ipv6 := mustParseIP(t, "fe80::a00:27ff:fe56:c24c")
58 |
59 | c.SaveMACFromIP(ipv6, nil)
60 |
61 | resolvedIPv4 := c.toIPv4(ipv6)
62 | if !resolvedIPv4.Equal(ipv4) {
63 | t.Errorf("IPv6 resolved to %s instead of %s", resolvedIPv4, ipv4)
64 | }
65 | }
66 |
67 | func TestCacheHostnames(t *testing.T) {
68 | c := NewCache()
69 |
70 | ipv4 := mustParseIP(t, "192.168.56.101")
71 | ipv6 := mustParseIP(t, "fe80::d422:2ab:8bf4:7381")
72 |
73 | // simulate previous lookup
74 | c.resolvedHostnames[ipv4.String()] = []string{"a"}
75 | c.resolvedHostnames[ipv6.String()] = []string{"b"}
76 | // lookup macs
77 | c.toMAC(ipv4)
78 | c.toMAC(ipv6)
79 |
80 | c.AddHostnamesForIP(ipv4, []string{"c"})
81 | c.AddHostnamesForIP(ipv4, []string{"c"})
82 | c.AddHostnamesForIP(ipv4, []string{"a"})
83 |
84 | c.AddHostnamesForIP(ipv6, []string{"d"})
85 |
86 | assertContainsExactly(t, c.Hostnames(ipv4), "a", "b", "c", "d")
87 | assertContainsExactly(t, c.Hostnames(ipv6), "a", "b", "c", "d")
88 | }
89 |
90 | func TestCacheToIPv4Ipv6(t *testing.T) {
91 | c := NewCache()
92 |
93 | ipv4 := mustParseIP(t, "192.168.56.101")
94 | ipv6 := mustParseIP(t, "fe80::d422:2ab:8bf4:7381")
95 |
96 | resolvedIPv4 := c.toIPv4(ipv6)
97 | if !resolvedIPv4.Equal(ipv4) {
98 | t.Errorf("resolved %s to %s instead of %s", ipv6, resolvedIPv4, ipv4)
99 | }
100 |
101 | resolvedIPv6 := c.toIPv6(ipv4)
102 | if !resolvedIPv6.Equal(ipv6) {
103 | t.Errorf("resolved %s to %s instead of %s", ipv4, resolvedIPv6, ipv6)
104 | }
105 | }
106 |
107 | func TestCacheMACResolution(t *testing.T) {
108 | c := NewCache()
109 |
110 | assertVendor(t, c, "08:00:27:00:00:01", "A")
111 | assertVendor(t, c, "08:00:27:c2:e0:27", "A")
112 | assertVendor(t, c, "08:00:27:ff:e0:27", "specific")
113 | assertVendor(t, c, "00:00:00:00:00:00", "")
114 | assertVendor(t, c, "52:54:00:52:54:00", "B")
115 | assertVendor(t, c, "0a:00:27:00:00:00", "test")
116 | assertVendor(t, c, "ff:ff:ff:ff:ff:ff", "C")
117 | assertVendor(t, c, "FF:FF:FF:FF:FF:FF", "C")
118 | }
119 |
120 | func TestLookupUsingExternalHostnames(t *testing.T) {
121 | c := NewCache()
122 |
123 | ipv4 := mustParseIP(t, "10.10.10.10")
124 | ipv6 := mustParseIP(t, "fe80::dead:beef")
125 | hostname := "testhost"
126 |
127 | // DHCPv6 message with hostname received
128 | c.AddHostnamesForIP(ipv6, []string{hostname})
129 |
130 | // the hostname will resolve both IPs
131 | c.resolvedIPs[hostname] = []net.IP{ipv4, ipv6}
132 |
133 | // resolve IPv4 via the lookup of the hostname associated with IPv6 address via DHCPv6
134 | resolvedIPv4 := c.toIPv4(ipv6)
135 | if !resolvedIPv4.Equal(ipv4) {
136 | t.Errorf("resolved %s to %s instead of %s", ipv6, resolvedIPv4, ipv4)
137 | }
138 | }
139 |
140 | func assertVendor(tb testing.TB, c *Cache, macString string, vendor string) {
141 | tb.Helper()
142 |
143 | mac, err := net.ParseMAC(macString)
144 | if err != nil {
145 | tb.Errorf("parse MAC %q: %v", macString, err)
146 |
147 | return
148 | }
149 |
150 | v := c.vendorByMAC(mac)
151 | if v != vendor {
152 | tb.Errorf("resolved MAC %s to vendor %q instead of %q", macString, v, vendor)
153 | }
154 | }
155 |
156 | func testGetMac(tb testing.TB, testOutputFileName string,
157 | resolver func(net.IP, []byte) net.HardwareAddr, testCases []macIPTestCase,
158 | ) {
159 | tb.Helper()
160 |
161 | content, err := os.ReadFile(testOutputFileName) //nolint:gosec
162 | if err != nil {
163 | tb.Fatalf("read proc file: %v", err)
164 | }
165 |
166 | for i, testCase := range testCases {
167 | ip := net.ParseIP(testCase.ip)
168 | if ip == nil && testCase.ip != "" {
169 | tb.Errorf("invalid input IP in testcase %d: %s", i, testCase.ip)
170 | }
171 |
172 | expectedMAC, err := net.ParseMAC(testCase.mac)
173 | if err != nil && testCase.mac != "" {
174 | tb.Errorf("invalid exepcted output MAC in testcase %d: %s", i, testCase.mac)
175 | }
176 |
177 | mac := resolver(ip, content)
178 | if mac.String() != expectedMAC.String() {
179 | tb.Errorf("get MAC of IP %s (%s): got %s instead of %s",
180 | ip, strings.TrimPrefix(testOutputFileName, "testdata/"), mac, expectedMAC)
181 | }
182 | }
183 | }
184 |
185 | func testGetIP(tb testing.TB, testOutputFileName string,
186 | resolver func(net.HardwareAddr, []byte) net.IP, testCases []macIPTestCase,
187 | ) {
188 | tb.Helper()
189 |
190 | content, err := os.ReadFile(testOutputFileName) //nolint:gosec
191 | if err != nil {
192 | tb.Fatalf("read proc file: %v", err)
193 | }
194 |
195 | for i, testCase := range testCases {
196 | mac, err := net.ParseMAC(testCase.mac)
197 | if err != nil && testCase.mac != "" {
198 | tb.Errorf("invalid exepcted output MAC in testcase %d: %s", i, testCase.mac)
199 | }
200 |
201 | expectedIP := net.ParseIP(testCase.ip)
202 | if expectedIP == nil && testCase.ip != "" {
203 | tb.Errorf("invalid input IP in testcase %d: %s", i, testCase.ip)
204 | }
205 |
206 | ip := resolver(mac, content)
207 | if ip.String() != expectedIP.String() {
208 | tb.Errorf("get IP of MAC %s (%s): got %s instead of %s",
209 | mac, strings.TrimPrefix(testOutputFileName, "testdata/"), ip, expectedIP)
210 | }
211 | }
212 | }
213 |
214 | func assertContainsExactly(tb testing.TB, got []string, required ...string) {
215 | tb.Helper()
216 |
217 | if len(got) != len(required) {
218 | tb.Errorf("got %+v elements instead of %+v", got, required)
219 | }
220 |
221 | for _, needle := range required {
222 | if !containsString(got, needle) {
223 | tb.Errorf("missing entry: %s", needle)
224 | }
225 | }
226 | }
227 |
228 | func containsString(haystack []string, needle string) bool {
229 | for _, element := range haystack {
230 | if element == needle {
231 | return true
232 | }
233 | }
234 |
235 | return false
236 | }
237 |
238 | func mustParseIP(tb testing.TB, ip string) net.IP {
239 | tb.Helper()
240 |
241 | parsedIP := net.ParseIP(ip)
242 | if parsedIP == nil {
243 | tb.Fatalf("cannot parse IP %s", ip)
244 | }
245 |
246 | return parsedIP
247 | }
248 |
--------------------------------------------------------------------------------
/log.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "net"
8 | "os"
9 | "strconv"
10 | "strings"
11 | "sync"
12 | "time"
13 |
14 | "github.com/RedTeamPentesting/pretender/hostinfo"
15 | "github.com/insomniacslk/dhcp/dhcpv6"
16 | )
17 |
18 | // escape is the ANSI escape sequence.
19 | const escape = "\x1b"
20 |
21 | // attribute represents a style.
22 | type attribute int
23 |
24 | // Base attributes.
25 | const (
26 | reset attribute = iota
27 | bold
28 | faint
29 | )
30 |
31 | // Foreground text colors.
32 | const (
33 | fgRed attribute = iota + 31
34 | fgGreen
35 | )
36 |
37 | type baseLogger struct {
38 | Verbose bool
39 | PrintTimestamps bool
40 | NoColor bool
41 | HostInfoCache *hostinfo.Cache
42 | HideIgnored bool
43 | NoHostInfo bool
44 |
45 | LogFile *os.File
46 | logFileMutex sync.Mutex
47 |
48 | wg sync.WaitGroup
49 | }
50 |
51 | func newBaseLogger() *baseLogger {
52 | return &baseLogger{
53 | PrintTimestamps: true,
54 | HostInfoCache: hostinfo.NewCache(),
55 | }
56 | }
57 |
58 | // Logger provides logging functionality.
59 | type Logger struct {
60 | *baseLogger
61 | Prefix string
62 | }
63 |
64 | // NewLogger returns a Logger.
65 | func NewLogger() *Logger {
66 | return &Logger{
67 | baseLogger: newBaseLogger(),
68 | }
69 | }
70 |
71 | func (l *Logger) style(attrs ...attribute) string {
72 | if l.NoColor {
73 | return ""
74 | }
75 |
76 | s := ""
77 | for _, a := range attrs {
78 | s += fmt.Sprintf("%s[%dm", escape, a)
79 | }
80 |
81 | return s
82 | }
83 |
84 | // WithPrefix returns a copy of the logger with a new prefix.
85 | func (l *Logger) WithPrefix(prefix string) *Logger {
86 | return &Logger{
87 | baseLogger: l.baseLogger,
88 | Prefix: prefix,
89 | }
90 | }
91 |
92 | // Debugf prints debug information.
93 | func (l *Logger) Debugf(format string, a ...interface{}) {
94 | if l == nil || !l.Verbose {
95 | return
96 | }
97 |
98 | l.logf(os.Stdout, l.styleAndPrefix(faint)+format, a...)
99 | }
100 |
101 | // Infof prints info messages.
102 | func (l *Logger) Infof(format string, a ...interface{}) {
103 | if l == nil {
104 | return
105 | }
106 |
107 | l.logf(os.Stdout, l.styleAndPrefix()+format, a...)
108 | }
109 |
110 | // Query prints query information.
111 | func (l *Logger) Query(name string, queryType string, peer net.IP) {
112 | if l == nil {
113 | return
114 | }
115 |
116 | l.logWithHostInfo(peer, func(hostInfo string) string {
117 | return fmt.Sprintf(l.styleAndPrefix(fgGreen)+"%q (%s) queried by %s", name, queryType, hostInfo)
118 | }, logFileEntry{
119 | Name: name,
120 | Type: l.Prefix,
121 | QueryType: queryType,
122 | Source: peer,
123 | })
124 | }
125 |
126 | // RefuseDynamicUpdate prints information about refused DNS dynamic updates.
127 | func (l *Logger) RefuseDynamicUpdate(name string, queryType string, peer net.IP) {
128 | if l == nil {
129 | return
130 | }
131 |
132 | l.logWithHostInfo(peer, func(hostInfo string) string {
133 | return fmt.Sprintf(l.styleAndPrefix(fgGreen)+"refusing SOA dynamic update of %q from %s", name, hostInfo)
134 | }, logFileEntry{
135 | Name: name,
136 | Type: l.Prefix,
137 | QueryType: queryType,
138 | Source: peer,
139 | })
140 | }
141 |
142 | // IgnoreDNS prints information abound ignored DNS queries.
143 | func (l *Logger) IgnoreDNS(name string, queryType string, peer net.IP, reason string) {
144 | if l == nil {
145 | return
146 | }
147 |
148 | l.logWithHostInfo(peer, func(hostInfo string) string {
149 | if l.HideIgnored {
150 | return ""
151 | }
152 |
153 | reasonSuffix := reason
154 | if reasonSuffix != "" {
155 | reasonSuffix = ": " + reasonSuffix
156 | }
157 |
158 | return fmt.Sprintf(l.styleAndPrefix()+l.style(faint)+"ignoring query for %q (%s) from %s%s",
159 | name, queryType, hostInfo, reasonSuffix)
160 | }, logFileEntry{
161 | Name: name,
162 | Type: l.Prefix,
163 | QueryType: queryType,
164 | Source: peer,
165 | Ignored: true,
166 | IgnoreReason: reason,
167 | })
168 | }
169 |
170 | // IgnoreDHCP prints information abound ignored DHCP requests.
171 | func (l *Logger) IgnoreDHCP(dhcpType string, peer peerInfo, reason string) {
172 | if l == nil {
173 | return
174 | }
175 |
176 | l.HostInfoCache.AddHostnamesForIP(peer.IP, peer.Hostnames)
177 |
178 | l.logWithHostInfo(peer.IP, func(hostInfo string) string {
179 | if l.HideIgnored {
180 | return ""
181 | }
182 |
183 | reasonSuffix := reason
184 | if reasonSuffix != "" {
185 | reasonSuffix = ": " + reasonSuffix
186 | }
187 |
188 | return fmt.Sprintf(l.styleAndPrefix()+l.style(faint)+"ignoring DHCP %s request from %s%s",
189 | dhcpType, hostInfo, reasonSuffix)
190 | }, logFileEntry{
191 | Source: peer.IP,
192 | Type: "DHCP",
193 | Ignored: true,
194 | IgnoreReason: reason,
195 | })
196 | }
197 |
198 | // DHCP prints information abound answered DHCP requests in which an address is assined.
199 | func (l *Logger) DHCP(dhcpType dhcpv6.MessageType, peer peerInfo, assignedAddress net.IP) {
200 | if l == nil {
201 | return
202 | }
203 |
204 | l.HostInfoCache.AddHostnamesForIP(peer.IP, peer.Hostnames)
205 |
206 | message := "responding to %s from %s by assigning "
207 | if dhcpType != dhcpv6.MessageTypeSolicit {
208 | message += "DNS server and "
209 | }
210 |
211 | message += "IPv6 %q"
212 |
213 | l.logWithHostInfo(peer.IP, func(hostInfo string) string {
214 | return fmt.Sprintf(l.styleAndPrefix()+l.style(faint)+message, dhcpType, hostInfo, assignedAddress)
215 | }, logFileEntry{
216 | AssignedAddress: assignedAddress,
217 | QueryType: dhcpType.String(),
218 | Source: peer.IP,
219 | Type: "DHCP",
220 | })
221 | }
222 |
223 | // Errorf prints errors.
224 | func (l *Logger) Errorf(format string, a ...interface{}) {
225 | if l == nil {
226 | return
227 | }
228 |
229 | l.logf(stdErr, l.styleAndPrefix(bold, fgRed)+format, a...)
230 | }
231 |
232 | // Fatalf prints fatal errors and quits the application without shutdown.
233 | func (l *Logger) Fatalf(format string, a ...interface{}) {
234 | if l == nil {
235 | return
236 | }
237 |
238 | l.logf(stdErr, l.styleAndPrefix(bold, fgRed)+format, a...)
239 | os.Exit(1)
240 | }
241 |
242 | // Flush blocks until all log messages are printed. Flush does not nessarily
243 | // flush the log file.
244 | func (l *Logger) Flush() {
245 | l.baseLogger.wg.Wait()
246 | }
247 |
248 | // Close performs a Flush() and closes and thereby flushes the log file if configured.
249 | func (l *Logger) Close() {
250 | l.Flush()
251 | defer l.Flush()
252 |
253 | if l.LogFile != nil {
254 | l.logFileMutex.Lock()
255 | err := l.LogFile.Close()
256 | l.logFileMutex.Unlock()
257 |
258 | l.LogFile = nil
259 |
260 | if err != nil {
261 | l.Errorf("closing log file: %v", err)
262 | l.Flush()
263 | }
264 | }
265 | }
266 |
267 | func (l *Logger) logWithHostInfo(peer net.IP, logString func(hostInfo string) string, logEntry logFileEntry) {
268 | l.baseLogger.wg.Add(1)
269 |
270 | if logEntry.Time.IsZero() {
271 | logEntry.Time = time.Now()
272 | }
273 |
274 | log := func() {
275 | hostInfo := peer.String()
276 |
277 | if !l.NoHostInfo {
278 | infos := l.HostInfoCache.HostInfos(peer)
279 | if len(infos) != 0 {
280 | hostInfo = fmt.Sprintf("%s (%s)", peer, strings.Join(infos, ", "))
281 | }
282 |
283 | logEntry.SourceInfo = infos
284 | }
285 |
286 | if l.LogFile != nil {
287 | fileLogLine, err := json.Marshal(logEntry)
288 | if err != nil {
289 | l.Errorf("marshalling file log entry: %v", err)
290 | }
291 |
292 | l.logFileMutex.Lock()
293 | _, err = l.LogFile.Write(append(fileLogLine, byte('\n')))
294 | l.logFileMutex.Unlock()
295 |
296 | if err != nil {
297 | l.Errorf("logging to file: %w", err)
298 | l.LogFile = nil
299 | }
300 | }
301 |
302 | l.logf(os.Stdout, logString(hostInfo))
303 |
304 | l.baseLogger.wg.Done()
305 | }
306 |
307 | if l.NoHostInfo {
308 | log()
309 | } else {
310 | go log()
311 | }
312 | }
313 |
314 | func (l *Logger) logf(w io.Writer, format string, a ...interface{}) {
315 | if l == nil || (format == "" && len(a) == 0) {
316 | return
317 | }
318 |
319 | if l.PrintTimestamps {
320 | format = fmt.Sprintf("%s%s%s %s", l.style(faint), time.Now().Format("15:04:05"), l.style(reset), format)
321 | }
322 |
323 | l.baseLogger.wg.Add(1)
324 |
325 | go func() {
326 | fmt.Fprintf(w, format+l.style(reset)+"\n", a...)
327 | l.baseLogger.wg.Done()
328 | }()
329 | }
330 |
331 | func (l *Logger) styleAndPrefix(attrs ...attribute) string {
332 | if l.Prefix == "" {
333 | return l.style(attrs...)
334 | }
335 |
336 | return fmt.Sprintf("%s%s[%s]%s%s ", l.style(attrs...), l.style(bold), l.Prefix, l.style(reset), l.style(attrs...))
337 | }
338 |
339 | func styled(text string, disableStyle bool, styles ...attribute) string {
340 | if disableStyle {
341 | return text
342 | }
343 |
344 | strStyles := make([]string, 0, len(styles))
345 | for _, s := range styles {
346 | strStyles = append(strStyles, strconv.Itoa(int(s)))
347 | }
348 |
349 | return fmt.Sprintf("%s[%sm%s%s[%dm", escape, strings.Join(strStyles, ";"), text, escape, reset)
350 | }
351 |
352 | type logFileEntry struct {
353 | Name string `json:"name,omitempty"`
354 | AssignedAddress net.IP `json:"assigned_addr,omitempty"`
355 | QueryType string `json:"query_type,omitempty"`
356 | Type string `json:"type"`
357 | Source net.IP `json:"source"`
358 | SourceInfo []string `json:"source_info"`
359 | Time time.Time `json:"time"`
360 | Ignored bool `json:"ignored"`
361 | IgnoreReason string `json:"ignore_reason,omitempty"`
362 | }
363 |
364 | func escapeFormatString(s string) string {
365 | return strings.ReplaceAll(s, "%", "%%")
366 | }
367 |
--------------------------------------------------------------------------------
/dns_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "net"
6 | "os"
7 | "testing"
8 | "time"
9 |
10 | "github.com/miekg/dns"
11 | )
12 |
13 | func TestDNS(t *testing.T) {
14 | testReply(t, "testdata/dns_request.bin", "testdata/dns_reply.bin")
15 | }
16 |
17 | func TestDNSAny(t *testing.T) {
18 | request := &dns.Msg{}
19 | request.SetQuestion("host", dns.TypeANY)
20 |
21 | relayIPv4 := mustParseIP(t, "10.0.0.2")
22 | relayIPv6 := mustParseIP(t, "fe80::1")
23 | mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(t, "10.0.0.1")}}
24 | cfgs := []Config{
25 | {RelayIPv4: relayIPv4, RelayIPv6: nil, TTL: 60 * time.Second},
26 | {RelayIPv4: nil, RelayIPv6: relayIPv6, TTL: 60 * time.Second},
27 | {RelayIPv4: relayIPv4, RelayIPv6: relayIPv6, TTL: 60 * time.Second},
28 | }
29 |
30 | for i, cfg := range cfgs {
31 | reply := createDNSReplyFromRequest(mockRW, request, nil, cfg)
32 | if reply == nil {
33 | t.Fatalf("config %d: no message was created", i)
34 |
35 | return // calm down staticcheck linter
36 | }
37 |
38 | expectedNumberOfAnswers := 0
39 |
40 | if cfg.RelayIPv4 != nil {
41 | expectedNumberOfAnswers++
42 |
43 | aAnswer, ok := getAnswerByType(t, reply.Answer, dns.TypeA).(*dns.A)
44 | if !ok {
45 | t.Fatalf("config %d: unexpected type for A answer", i)
46 | }
47 |
48 | assertRRHeader(t, aAnswer.Hdr, "host", dns.TypeA, cfg.TTL)
49 |
50 | if !aAnswer.A.Equal(relayIPv4) {
51 | t.Fatalf("config %d: abnswer contains A address %s instead of %s",
52 | i, aAnswer.A, relayIPv4)
53 | }
54 | }
55 |
56 | if cfg.RelayIPv6 != nil {
57 | expectedNumberOfAnswers++
58 |
59 | aaaaAnswer, ok := getAnswerByType(t, reply.Answer, dns.TypeAAAA).(*dns.AAAA)
60 | if !ok {
61 | t.Fatalf("config %d: unexpected type for A answer", i)
62 | }
63 |
64 | assertRRHeader(t, aaaaAnswer.Hdr, "host", dns.TypeAAAA, cfg.TTL)
65 |
66 | if !aaaaAnswer.AAAA.Equal(relayIPv6) {
67 | t.Fatalf("config %d: abnswer contains AAAA address %s instead of %s",
68 | i, aaaaAnswer.AAAA, relayIPv6)
69 | }
70 | }
71 |
72 | if len(reply.Answer) != expectedNumberOfAnswers {
73 | t.Fatalf("config %d: unexpected number of answers: %d instead of %d",
74 | i, len(reply.Answer), expectedNumberOfAnswers)
75 | }
76 | }
77 | }
78 |
79 | //nolint:cyclop
80 | func TestDNSSOA(t *testing.T) {
81 | soa := &dns.Msg{}
82 | soa.SetQuestion("host", dns.TypeSOA)
83 |
84 | relayIPv4 := mustParseIP(t, "10.0.0.2")
85 | relayIPv6 := mustParseIP(t, "fe80::1")
86 | mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(t, "10.0.0.1")}}
87 |
88 | cfg := Config{RelayIPv4: relayIPv4, RelayIPv6: relayIPv6, TTL: 60 * time.Second}
89 |
90 | // don't respond to SOA when no SOA hostname is configured
91 | noReply := createDNSReplyFromRequest(mockRW, soa, nil, cfg)
92 | if noReply != nil {
93 | t.Fatalf("SOA rely was created without configuring SOA hostname")
94 | }
95 |
96 | cfg.SOAHostname = "hostname"
97 |
98 | reply := createDNSReplyFromRequest(mockRW, soa, nil, cfg)
99 | if reply == nil {
100 | t.Fatalf("no SOA reply was created")
101 | }
102 |
103 | soaAnswer, ok := getAnswerByType(t, reply.Answer, dns.TypeSOA).(*dns.SOA)
104 | if !ok {
105 | t.Fatalf("SOA answer has unexpected type")
106 | }
107 |
108 | assertRRHeader(t, soaAnswer.Hdr, "host", dns.TypeSOA, cfg.TTL)
109 |
110 | if soaAnswer.Ns != dns.Fqdn(cfg.SOAHostname) {
111 | t.Fatalf("unexpected SOA answer hostname: %q instead of %q",
112 | soaAnswer.Ns, dns.Fqdn(cfg.SOAHostname))
113 | }
114 |
115 | soaNS, ok := getAnswerByType(t, reply.Ns, dns.TypeNS).(*dns.NS)
116 | if !ok {
117 | t.Fatalf("SOA NS has unexpected type")
118 | }
119 |
120 | assertRRHeader(t, soaNS.Hdr, "host", dns.TypeNS, cfg.TTL)
121 |
122 | if soaNS.Ns != dns.Fqdn(cfg.SOAHostname) {
123 | t.Fatalf("unexpected SOA NS hostname: %q instead of %q",
124 | soaNS.Ns, dns.Fqdn(cfg.SOAHostname))
125 | }
126 |
127 | soaA, ok := getAnswerByType(t, reply.Extra, dns.TypeA).(*dns.A)
128 | if !ok {
129 | t.Fatalf("SOA A record has unexpected type")
130 | }
131 |
132 | assertRRHeader(t, soaA.Hdr, dns.Fqdn(cfg.SOAHostname), dns.TypeA, cfg.TTL)
133 |
134 | if !soaA.A.Equal(relayIPv4) {
135 | t.Fatalf("SOA extra A record contains address %s instead of %s",
136 | soaA.A, relayIPv4)
137 | }
138 |
139 | soaAAAA, ok := getAnswerByType(t, reply.Extra, dns.TypeAAAA).(*dns.AAAA)
140 | if !ok {
141 | t.Fatalf("SOA AAAA record has unexpected type")
142 | }
143 |
144 | assertRRHeader(t, soaAAAA.Hdr, dns.Fqdn(cfg.SOAHostname), dns.TypeAAAA, cfg.TTL)
145 |
146 | if !soaAAAA.AAAA.Equal(relayIPv6) {
147 | t.Fatalf("SOA extra AAAA record contains address %s instead of %s",
148 | soaAAAA.AAAA, relayIPv6)
149 | }
150 | }
151 |
152 | func TestDNSSOADynamicUpdate(t *testing.T) {
153 | soa := &dns.Msg{}
154 | soa.SetQuestion("host", dns.TypeSOA)
155 | soa.Opcode = dns.OpcodeUpdate
156 |
157 | relayIPv4 := mustParseIP(t, "10.0.0.2")
158 | relayIPv6 := mustParseIP(t, "fe80::1")
159 | mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(t, "10.0.0.1")}}
160 |
161 | cfg := Config{
162 | RelayIPv4: relayIPv4,
163 | RelayIPv6: relayIPv6,
164 | TTL: 60 * time.Second,
165 | SOAHostname: "hostname",
166 | }
167 |
168 | reply := createDNSReplyFromRequest(mockRW, soa, nil, cfg)
169 | if reply == nil {
170 | t.Fatalf("no SOA reply was created")
171 | }
172 |
173 | if reply.Rcode != dns.RcodeRefused {
174 | t.Fatalf("SOA dynamic update was not refused")
175 | }
176 | }
177 |
178 | //nolint:cyclop
179 | func testReply(tb testing.TB, requestFileName string, replyFileName string) {
180 | tb.Helper()
181 |
182 | relayIPv4 := mustParseIP(tb, "10.0.0.2")
183 | relayIPv6 := mustParseIP(tb, "fe80::1")
184 | mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(tb, "10.0.0.1")}}
185 | request := readNameServiceMessage(tb, requestFileName)
186 | expectedReply := readFile(tb, replyFileName)
187 | cfg := Config{RelayIPv4: relayIPv4, RelayIPv6: relayIPv6, TTL: 60 * time.Second}
188 |
189 | reply := createDNSReplyFromRequest(mockRW, request, nil, cfg)
190 | if reply == nil {
191 | tb.Fatalf("no message was created")
192 |
193 | return // calm down staticcheck linter
194 | }
195 |
196 | if len(reply.Answer) == 0 {
197 | tb.Fatalf("reply contains no answers")
198 | }
199 |
200 | for _, answer := range reply.Answer {
201 | switch a := answer.(type) {
202 | case *dns.A:
203 | if !a.A.Equal(relayIPv4) {
204 | tb.Fatalf("reply contains A address %s instead of %s", a.A, relayIPv4)
205 | }
206 |
207 | if a.Hdr.Rrtype != dns.TypeA {
208 | tb.Fatalf("unexpected type: got %s instead of %s", dnsQueryType(a.Hdr.Rrtype), dnsQueryType(dns.TypeA))
209 | }
210 | case *dns.AAAA:
211 | if !a.AAAA.Equal(relayIPv6) {
212 | tb.Fatalf("reply contains AAAA address %s instead of %s", a.AAAA, relayIPv6)
213 | }
214 |
215 | if a.Hdr.Rrtype != dns.TypeAAAA {
216 | tb.Fatalf("unexpected type: got %s instead of %s", dnsQueryType(a.Hdr.Rrtype), dnsQueryType(dns.TypeAAAA))
217 | }
218 | default:
219 | tb.Fatalf("unexpected reply type %T", answer)
220 | }
221 |
222 | answerHeader := answer.Header()
223 |
224 | if answerHeader.Ttl != uint32(cfg.TTL.Seconds()) {
225 | tb.Fatalf("unexpected TTL: got %ds instead of %ds", answerHeader.Ttl, uint32(cfg.TTL.Seconds()))
226 | }
227 |
228 | if answerHeader.Name != request.Question[0].Name {
229 | tb.Fatalf("%q was echoed instead of question name %q", answerHeader.Name, reply.Question[0].Name)
230 | }
231 |
232 | if answerHeader.Class != dns.ClassINET {
233 | tb.Fatalf("unexpected class: got %d instead of %d", answerHeader.Class, dns.ClassINET)
234 | }
235 | }
236 |
237 | replyBuffer, err := reply.Pack()
238 | if err != nil {
239 | tb.Fatalf("pack reply: %v", err)
240 | }
241 |
242 | if !bytes.Equal(replyBuffer, expectedReply) {
243 | tb.Fatalf("reply bytes do not match")
244 | }
245 | }
246 |
247 | func getAnswerByType(tb testing.TB, answers []dns.RR, answerType uint16) dns.RR { //nolint:ireturn
248 | tb.Helper()
249 |
250 | for _, answer := range answers {
251 | if answer.Header().Rrtype == answerType {
252 | return answer
253 | }
254 | }
255 |
256 | tb.Fatalf("found no answer of type %d", answerType)
257 |
258 | return nil
259 | }
260 |
261 | func readNameServiceMessage(tb testing.TB, fileName string) *dns.Msg {
262 | tb.Helper()
263 |
264 | msg := &dns.Msg{}
265 |
266 | err := msg.Unpack(readFile(tb, fileName))
267 | if err != nil {
268 | tb.Fatalf("parse NetBIOS test file: %v", err)
269 | }
270 |
271 | return msg
272 | }
273 |
274 | func readFile(tb testing.TB, fileName string) []byte {
275 | tb.Helper()
276 |
277 | content, err := os.ReadFile(fileName) //nolint:gosec
278 | if err != nil {
279 | tb.Fatalf("read file: %v", err)
280 | }
281 |
282 | return content
283 | }
284 |
285 | func assertRRHeader(tb testing.TB, hdr dns.RR_Header, name string, rtype uint16, ttl time.Duration) {
286 | tb.Helper()
287 |
288 | if hdr.Name != name {
289 | tb.Fatalf("unexpected name in header: %q instead of %q", hdr.Name, name)
290 | }
291 |
292 | if hdr.Rrtype != rtype {
293 | tb.Fatalf("unexpected type in header: %d instead of %d", hdr.Rrtype, rtype)
294 | }
295 |
296 | if hdr.Class != dns.ClassINET {
297 | tb.Fatalf("unexpected class in header: %d instead of %d", hdr.Class, dns.ClassINET)
298 | }
299 |
300 | if hdr.Ttl != uint32(ttl.Seconds()) {
301 | tb.Fatalf("unexpected TTL in header: %ds instead of %s", hdr.Ttl, ttl)
302 | }
303 | }
304 |
305 | type mockResonseWriter struct {
306 | Local net.Addr
307 | Remote net.Addr
308 | }
309 |
310 | var _ dns.ResponseWriter = mockResonseWriter{}
311 |
312 | func (m mockResonseWriter) LocalAddr() net.Addr {
313 | return m.Local
314 | }
315 |
316 | func (m mockResonseWriter) RemoteAddr() net.Addr {
317 | return m.Remote
318 | }
319 |
320 | func (mockResonseWriter) WriteMsg(*dns.Msg) error {
321 | return nil
322 | }
323 |
324 | func (mockResonseWriter) Write([]byte) (int, error) {
325 | return 0, nil
326 | }
327 |
328 | func (m mockResonseWriter) Close() error {
329 | return nil
330 | }
331 |
332 | func (m mockResonseWriter) TsigStatus() error {
333 | return nil
334 | }
335 |
336 | func (m mockResonseWriter) TsigTimersOnly(bool) {}
337 |
338 | func (m mockResonseWriter) Hijack() {}
339 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
4 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
5 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
6 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
7 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
8 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
9 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
10 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
11 | github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
12 | github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c h1:OCFM4+DXTWfNlyeoddrTwdup/ztkGSyAMR2UGcPckNQ=
13 | github.com/insomniacslk/dhcp v0.0.0-20221001123530-5308ebe5334c/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
14 | github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
15 | github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
16 | github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
17 | github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
18 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
19 | github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
20 | github.com/mdlayher/ndp v1.0.0 h1:rcFaJVj04Rj47ZlV/t3iZcuKzlpwBuBsD3gR9AHDzcI=
21 | github.com/mdlayher/ndp v1.0.0/go.mod h1:+3vkk6YnlL8ZTRTjmQanCNQFqDKOpP2zNyHl2HqyoZs=
22 | github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
23 | github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
24 | github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
25 | github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
26 | github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
27 | github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
28 | github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
29 | github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
32 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
33 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
34 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
35 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
36 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
38 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
39 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
40 | github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
41 | github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 h1:hl6sK6aFgTLISijk6xIzeqnPzQcsLqqvL6vEfTPinME=
42 | github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
43 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
44 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
45 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
46 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
47 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
48 | golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
49 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
50 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
51 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
52 | golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
53 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
54 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
55 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
56 | golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
57 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
58 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
59 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
60 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
61 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
62 | golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
63 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
64 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
65 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
66 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
67 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
68 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
69 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
70 | golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
71 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
72 | golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
73 | golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
74 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
75 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
76 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
77 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
78 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
79 | golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
80 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
81 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
82 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
83 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
84 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
85 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
86 | golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
87 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
88 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
89 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
90 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
91 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
92 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
93 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
94 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
95 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
96 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
97 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
98 | golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
99 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
100 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
101 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
102 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
103 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
104 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
105 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
106 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
107 |
--------------------------------------------------------------------------------
/dns.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "strconv"
8 | "strings"
9 | "time"
10 |
11 | "github.com/miekg/dns"
12 | "golang.org/x/sync/errgroup"
13 | )
14 |
15 | const (
16 | dnsPort = 53 // default DNS port
17 |
18 | // DefaultTTL is the time to live specified in replies to name resolution
19 | // queries of any type.
20 | dnsDefaultTTL = 60 * time.Second
21 |
22 | // NetBIOS name resolution messages have the type set to 32 which means
23 | // NIMLOC in the DNS spec. In this case we don't expect actual NIMLOC
24 | // messages, so we assume type 32 messages to be NetBIOS requests.
25 | typeNetBios = dns.TypeNIMLOC
26 | )
27 |
28 | //nolint:cyclop
29 | func createDNSReplyFromRequest(rw dns.ResponseWriter, request *dns.Msg, logger *Logger, config Config) *dns.Msg {
30 | reply := &dns.Msg{}
31 | reply.SetReply(request)
32 | reply.Authoritative = true // this has to be set for Windows to accept NetBIOS queries
33 |
34 | peer, err := toIP(rw.RemoteAddr())
35 | if err != nil {
36 | logger.Errorf(err.Error())
37 |
38 | return nil
39 | }
40 |
41 | var peerHostnames []string
42 | if logger != nil && logger.HostInfoCache != nil {
43 | peerHostnames = logger.HostInfoCache.Hostnames(peer)
44 | }
45 |
46 | for _, q := range request.Question {
47 | name := normalizedNameFromQuery(q)
48 |
49 | shouldRespond, reason := shouldRespondToNameResolutionQuery(config, name, q.Qtype, peer, peerHostnames)
50 | if !shouldRespond {
51 | logger.IgnoreDNS(name, queryType(q, request.Opcode), peer, reason)
52 |
53 | continue
54 | }
55 |
56 | switch q.Qtype {
57 | case dns.TypeA:
58 | reply.Answer = append(reply.Answer, rr(config.RelayIPv4, q.Name, config.TTL))
59 | case dns.TypeAAAA:
60 | reply.Answer = append(reply.Answer, rr(config.RelayIPv6, q.Name, config.TTL))
61 | case dns.TypeSOA:
62 | switch {
63 | case config.SOAHostname == "":
64 | logger.IgnoreDNS(name, queryType(q, request.Opcode), peer, "no SOA hostname configured")
65 |
66 | continue
67 | case request.Opcode == dns.OpcodeUpdate:
68 | // Refuse dynamic update to trigger authentication in TKEY query over TCP (not handled by pretender)
69 | reply.Rcode = dns.RcodeRefused
70 | reply.Ns = request.Ns
71 | reply.Answer = nil
72 |
73 | logger.RefuseDynamicUpdate(name, queryType(q, request.Opcode), peer)
74 |
75 | return reply // no need to react the other questions
76 | default:
77 | // Tell the client that a server with the hostname `SOAHostname`
78 | // is authoritative for the requested zone. And, btw: WE pretend
79 | // to be the host named `SOAHostname`.
80 | soaHostname := dns.Fqdn(config.SOAHostname)
81 |
82 | reply.Answer = append(reply.Answer, &dns.SOA{
83 | Hdr: rrHeader(q.Name, dns.TypeSOA, config.TTL),
84 | Ns: soaHostname,
85 | Mbox: "pretender.invalid.",
86 | })
87 |
88 | reply.Ns = append(reply.Ns, &dns.NS{
89 | Hdr: rrHeader(q.Name, dns.TypeNS, config.TTL),
90 | Ns: soaHostname,
91 | })
92 |
93 | if config.RelayIPv4 != nil {
94 | reply.Extra = append(reply.Extra, rr(config.RelayIPv4, soaHostname, config.TTL))
95 | }
96 |
97 | if config.RelayIPv6 != nil {
98 | reply.Extra = append(reply.Extra, rr(config.RelayIPv6, soaHostname, config.TTL))
99 | }
100 | }
101 | case dns.TypeANY:
102 | if config.RelayIPv4 != nil {
103 | reply.Answer = append(reply.Answer, rr(config.RelayIPv4, q.Name, config.TTL))
104 | }
105 |
106 | if config.RelayIPv6 != nil {
107 | reply.Answer = append(reply.Answer, rr(config.RelayIPv6, q.Name, config.TTL))
108 | }
109 | case typeNetBios:
110 | reply.CheckingDisabled = false
111 | reply.Question = nil
112 | reply.Answer = append(reply.Answer, &dns.NIMLOC{
113 | Hdr: rrHeader(q.Name, dns.TypeNIMLOC, config.TTL),
114 | Locator: encodeNetBIOSLocator(config.RelayIPv4.To4()),
115 | })
116 | default:
117 | logger.Debugf("%s query for name %s from %s is unhandled",
118 | dns.Type(q.Qtype).String(), name, rw.RemoteAddr().String())
119 |
120 | continue
121 | }
122 |
123 | logger.Query(name, queryType(q, request.Opcode), peer)
124 | }
125 |
126 | // don't send a reply at all if we don't actually spoof anything
127 | if len(reply.Answer) == 0 && len(reply.Ns) == 0 && len(reply.Extra) == 0 {
128 | logger.Debugf("ignoring query from %s because no answers were configured", rw.RemoteAddr().String())
129 |
130 | return nil
131 | }
132 |
133 | return reply
134 | }
135 |
136 | func rr(ip net.IP, name string, ttl time.Duration) dns.RR { //nolint:ireturn
137 | if ip.To4() == nil {
138 | return &dns.AAAA{Hdr: rrHeader(name, dns.TypeAAAA, ttl), AAAA: ip}
139 | }
140 |
141 | return &dns.A{Hdr: rrHeader(name, dns.TypeA, ttl), A: ip}
142 | }
143 |
144 | func rrHeader(name string, rtype uint16, ttl time.Duration) dns.RR_Header {
145 | return dns.RR_Header{Name: name, Rrtype: rtype, Class: dns.ClassINET, Ttl: uint32(ttl.Seconds())}
146 | }
147 |
148 | func toIP(addr net.Addr) (net.IP, error) {
149 | switch a := addr.(type) {
150 | case *net.TCPAddr:
151 | return a.IP, nil
152 | case *net.UDPAddr:
153 | return a.IP, nil
154 | default:
155 | return nil, fmt.Errorf("cannot extract IP from %T", addr)
156 | }
157 | }
158 |
159 | func normalizedNameFromQuery(q dns.Question) string {
160 | if q.Qtype == typeNetBios {
161 | return decodeNetBIOSHostname(q.Name)
162 | }
163 |
164 | name := normalizedName(q.Name)
165 |
166 | if name == "" {
167 | return q.Name
168 | }
169 |
170 | return name
171 | }
172 |
173 | func normalizedName(host string) string {
174 | return strings.TrimSuffix(strings.TrimSpace(host), ".")
175 | }
176 |
177 | func queryType(q dns.Question, opcode int) string {
178 | if q.Qtype == typeNetBios {
179 | return decodeNetBIOSSuffix(q.Name)
180 | }
181 |
182 | typeStr := dnsQueryType(q.Qtype)
183 |
184 | switch opcode {
185 | case dns.OpcodeStatus:
186 | return typeStr + " Status"
187 | case dns.OpcodeNotify:
188 | return typeStr + " Notify"
189 | case dns.OpcodeUpdate:
190 | return typeStr + " Dynamic Update"
191 | default:
192 | return typeStr
193 | }
194 | }
195 |
196 | func dnsQueryType(qtype uint16) string {
197 | return dns.Type(qtype).String()
198 | }
199 |
200 | // DNSHandler creates a dns.HandlerFunc based on the logic in
201 | // createReplyFromRequest.
202 | func DNSHandler(logger *Logger, config Config) dns.HandlerFunc {
203 | return func(rw dns.ResponseWriter, request *dns.Msg) {
204 | reply := createDNSReplyFromRequest(rw, request, logger, config)
205 | if reply == nil {
206 | _ = rw.Close() // early abort for TCP connections
207 |
208 | return
209 | }
210 |
211 | err := rw.WriteMsg(reply)
212 | if err != nil {
213 | logger.Errorf("writing reply: %v", err)
214 | }
215 | }
216 | }
217 |
218 | // UDPConnDNSHandler handles requests by creating a response using
219 | // createReplyFromRequest and sends it directly using the underlying UDP
220 | // connection on which the server operates.
221 | func UDPConnDNSHandler(conn net.PacketConn, logger *Logger, config Config) dns.HandlerFunc {
222 | return func(rw dns.ResponseWriter, request *dns.Msg) {
223 | reply := createDNSReplyFromRequest(rw, request, logger, config)
224 | if reply == nil {
225 | return
226 | }
227 |
228 | buf, err := reply.Pack()
229 | if err != nil {
230 | logger.Errorf("pack message: %v", err)
231 |
232 | return
233 | }
234 |
235 | _, err = conn.WriteTo(buf, rw.RemoteAddr())
236 | if err != nil {
237 | logger.Errorf("write dns reply: %v", err)
238 |
239 | return
240 | }
241 | }
242 | }
243 |
244 | // RunDNSResponder starts a TCP and a UDP DNS server.
245 | func RunDNSResponder(ctx context.Context, logger *Logger, config Config) error {
246 | errGroup, ctx := errgroup.WithContext(ctx)
247 |
248 | ipv6Addr := net.IPAddr{IP: config.LocalIPv6, Zone: config.Interface.Name}
249 | fullAddr := net.JoinHostPort(ipv6Addr.String(), strconv.Itoa(dnsPort))
250 |
251 | errGroup.Go(func() error {
252 | logger.Infof("listening via UDP on %s", fullAddr)
253 |
254 | return runDNSServerWithContext(ctx, &dns.Server{
255 | Addr: fullAddr,
256 | Net: "udp6",
257 | Handler: DNSHandler(logger, config),
258 | MsgAcceptFunc: acceptAllQueries,
259 | })
260 | })
261 |
262 | errGroup.Go(func() error {
263 | logger.Infof("listening via TCP on %s", fullAddr)
264 |
265 | return runDNSServerWithContext(ctx, &dns.Server{
266 | Addr: fullAddr,
267 | Net: "tcp6",
268 | Handler: DNSHandler(logger, config),
269 | MsgAcceptFunc: acceptAllQueries,
270 | })
271 | })
272 |
273 | // listen on IPv4 port only if we expect to receive a dynamic update to the
274 | // relay IPv4 address following an SOA query
275 | if config.SOAHostname != "" && hasSpecificIPv4Address(config.Interface, config.RelayIPv4) {
276 | fullAddr := net.JoinHostPort(config.RelayIPv4.String(), strconv.Itoa(dnsPort))
277 |
278 | errGroup.Go(func() error {
279 | logger.Infof("listening via TCP on %s", fullAddr)
280 |
281 | return runDNSServerWithContext(ctx, &dns.Server{
282 | Addr: fullAddr,
283 | Net: "udp4",
284 | Handler: DNSHandler(logger, config),
285 | MsgAcceptFunc: acceptAllQueries,
286 | })
287 | })
288 | }
289 |
290 | return errGroup.Wait()
291 | }
292 |
293 | func acceptAllQueries(dh dns.Header) dns.MsgAcceptAction {
294 | queryReplyBit := uint16(1 << 15) //nolint:gomnd
295 |
296 | if isReply := dh.Bits&queryReplyBit != 0; isReply {
297 | return dns.MsgIgnore
298 | }
299 |
300 | return dns.MsgAccept
301 | }
302 |
303 | func runDNSServerWithContext(ctx context.Context, server *dns.Server) error {
304 | go func() {
305 | <-ctx.Done()
306 |
307 | _ = server.Shutdown()
308 | }()
309 |
310 | err := server.ListenAndServe()
311 | if err != nil {
312 | return fmt.Errorf("activate and serve: %w", err)
313 | }
314 |
315 | return nil
316 | }
317 |
318 | // RunDNSHandlerOnUDPConnection runs the DNS handler on an arbitrary UDP
319 | // connection, such that the DNS handling logic can be used for LLMNR, mDNS and
320 | // NetBIOS name resolution.
321 | func RunDNSHandlerOnUDPConnection(ctx context.Context, conn net.PacketConn, logger *Logger, config Config) error {
322 | server := &dns.Server{
323 | PacketConn: conn,
324 | Handler: UDPConnDNSHandler(conn, logger, config),
325 | }
326 |
327 | go func() {
328 | <-ctx.Done()
329 |
330 | _ = server.Shutdown()
331 | }()
332 |
333 | err := server.ActivateAndServe()
334 | if err != nil {
335 | return fmt.Errorf("activate and serve: %w", err)
336 | }
337 |
338 | return nil
339 | }
340 |
341 | func hasSpecificIPv4Address(iface *net.Interface, ip net.IP) bool {
342 | addrs, err := iface.Addrs()
343 | if err != nil {
344 | return false
345 | }
346 |
347 | for _, addr := range addrs {
348 | ifIP, ok := addr.(*net.IPNet)
349 | if !ok {
350 | continue
351 | }
352 |
353 | if ifIP.IP.Equal(ip) {
354 | return true
355 | }
356 | }
357 |
358 | return false
359 | }
360 |
--------------------------------------------------------------------------------
/hostinfo/host_info.go:
--------------------------------------------------------------------------------
1 | // Package hostinfo can be used to correlate IPs, MACs and hostnames using
2 | // caches DNS lookups and ARP information.
3 | package hostinfo
4 |
5 | import (
6 | "bufio"
7 | "bytes"
8 | "context"
9 | _ "embed"
10 | "net"
11 | "os"
12 | "os/exec"
13 | "runtime"
14 | "sort"
15 | "strings"
16 | "sync"
17 | "time"
18 | )
19 |
20 | //go:generate python3 generate_mac_vendors.py
21 |
22 | const (
23 | execTimeoutWindows = 800 * time.Millisecond
24 | execTimeoutLinux = 250 * time.Millisecond
25 |
26 | osWindows = "windows"
27 | osLinux = "linux"
28 | )
29 |
30 | var testMode = false
31 |
32 | //go:embed mac-vendors.txt
33 | var macVendorsFile string
34 |
35 | type macIPPair struct {
36 | IP net.IP
37 | MAC net.HardwareAddr
38 | }
39 |
40 | // Cache caches information such as IPv4 addresses, MAC vendors and hostnames.
41 | type Cache struct {
42 | macIPPairs []macIPPair
43 | resolvedHostnames map[string][]string
44 | externalHostnames map[string][]string
45 | resolvedIPs map[string][]net.IP
46 | macVendors map[string]string
47 | macPrefixSizes []int
48 |
49 | sync.Mutex
50 | }
51 |
52 | // NewCache returns a new HostInfoCache and parses the embedded MAC vendors file.
53 | func NewCache() *Cache {
54 | cache := &Cache{
55 | macIPPairs: []macIPPair{},
56 | resolvedHostnames: map[string][]string{},
57 | externalHostnames: map[string][]string{},
58 | resolvedIPs: map[string][]net.IP{},
59 | macVendors: map[string]string{},
60 | }
61 |
62 | prefixSizes := map[int]struct{}{}
63 |
64 | scanner := bufio.NewScanner(strings.NewReader(macVendorsFile))
65 | for scanner.Scan() {
66 | line := scanner.Text()
67 | if len(line) == 0 || line[0] == '#' {
68 | continue
69 | }
70 |
71 | parts := strings.SplitN(line, "\t", 3) //nolint:gomnd
72 | if len(parts) < 2 { //nolint:gomnd
73 | continue
74 | }
75 |
76 | macPrefix := strings.ToUpper(parts[0])
77 |
78 | prefixSizes[len(macPrefix)] = struct{}{}
79 |
80 | cache.macVendors[macPrefix] = parts[1]
81 | }
82 |
83 | cache.macPrefixSizes = sortedHighToLow(prefixSizes)
84 |
85 | return cache
86 | }
87 |
88 | // SaveMACFromIP saves the MAC address extracted from the IP together with the
89 | // IP in the cache. If the IP did not contain an EUI-64 encoded MAC address a
90 | // MAC address with the first three bytes of the provided fallback address is
91 | // saved in order to be able to identify the vendor.
92 | func (c *Cache) SaveMACFromIP(ip net.IP, fallback net.HardwareAddr) {
93 | c.Lock()
94 | defer c.Unlock()
95 |
96 | for _, pair := range c.macIPPairs {
97 | if ip.Equal(pair.IP) {
98 | return
99 | }
100 | }
101 |
102 | mac := extractMAC(ip)
103 | if mac == nil {
104 | if fallback == nil {
105 | return
106 | }
107 |
108 | // use fallback only for vendor prefix
109 | mac = net.HardwareAddr{fallback[0], fallback[1], fallback[2], 0, 0, 0}
110 | }
111 |
112 | c.macIPPairs = append(c.macIPPairs, macIPPair{IP: ip, MAC: mac})
113 | }
114 |
115 | // AddHostnamesForIP registers a set of hostnames for a given IP that were
116 | // obtained externally. Hostnames are only added if they are not already
117 | // present.
118 | func (c *Cache) AddHostnamesForIP(ip net.IP, hostnames []string) {
119 | c.Lock()
120 | defer c.Unlock()
121 |
122 | c.externalHostnames[ip.String()] = appendUnique(c.externalHostnames[ip.String()], hostnames...)
123 | }
124 |
125 | func (c *Cache) toMAC(ip net.IP) net.HardwareAddr {
126 | for _, pair := range c.macIPPairs {
127 | if ip.Equal(pair.IP) {
128 | return pair.MAC
129 | }
130 | }
131 |
132 | mac := getMACFromOS(ip)
133 | if mac != nil {
134 | c.macIPPairs = append(c.macIPPairs, macIPPair{IP: ip, MAC: mac})
135 | }
136 |
137 | return mac
138 | }
139 |
140 | func (c *Cache) toIPv4(ip net.IP) net.IP {
141 | if ip.To4() != nil {
142 | return ip
143 | }
144 |
145 | mac := c.toMAC(ip)
146 | if mac == nil {
147 | return c.lookupUsingExternalHostnames(ip, mac)
148 | }
149 |
150 | for _, pair := range c.macIPPairs {
151 | if bytes.Equal(mac, pair.MAC) && pair.IP.To4() != nil {
152 | return pair.IP
153 | }
154 | }
155 |
156 | ipv4 := getIPFromARP(mac)
157 | if ipv4 != nil {
158 | c.macIPPairs = append(c.macIPPairs, macIPPair{MAC: mac, IP: ipv4})
159 |
160 | return ipv4
161 | }
162 |
163 | if ipv4 == nil {
164 | return c.lookupUsingExternalHostnames(ip, mac)
165 | }
166 |
167 | return ipv4
168 | }
169 |
170 | func (c *Cache) lookupUsingExternalHostnames(ip net.IP, mac net.HardwareAddr) net.IP {
171 | externalHostnames := c.externalHostnames[ip.String()]
172 | if len(externalHostnames) == 0 {
173 | return nil
174 | }
175 |
176 | // for now, only consider the first to avoid a lot of DNS requests
177 | externalHostname := externalHostnames[0]
178 |
179 | resolvedIPs, ok := c.resolvedIPs[externalHostname]
180 | if !ok {
181 | resolvedIPs = lookup(externalHostname)
182 |
183 | c.resolvedIPs[externalHostname] = resolvedIPs
184 | }
185 |
186 | if mac != nil {
187 | for _, resolvedIP := range resolvedIPs {
188 | c.macIPPairs = append(c.macIPPairs, macIPPair{MAC: mac, IP: resolvedIP})
189 | }
190 | }
191 |
192 | for _, resolvedIP := range resolvedIPs {
193 | if resolvedIP.To4() != nil {
194 | return resolvedIP
195 | }
196 | }
197 |
198 | return nil
199 | }
200 |
201 | func (c *Cache) toIPv6(ip net.IP) net.IP {
202 | if ip.To4() == nil {
203 | return ip
204 | }
205 |
206 | mac := c.toMAC(ip)
207 |
208 | for _, pair := range c.macIPPairs {
209 | if bytes.Equal(mac, pair.MAC) && pair.IP.To4() == nil {
210 | return pair.IP
211 | }
212 | }
213 |
214 | return nil
215 | }
216 |
217 | // Hostnames returns all known hostnames associated with the given IP.
218 | func (c *Cache) Hostnames(ip net.IP) []string {
219 | c.Lock()
220 | defer c.Unlock()
221 |
222 | return c.hostnames(ip)
223 | }
224 |
225 | func (c *Cache) hostnames(ip net.IP) []string {
226 | var results []string
227 |
228 | ipv4 := c.toIPv4(ip)
229 | if ipv4 != nil {
230 | hostnames, ok := c.resolvedHostnames[ipv4.String()]
231 | if !ok {
232 | hostnames = reverseLookup(ipv4.String())
233 |
234 | c.resolvedHostnames[ipv4.String()] = hostnames
235 | }
236 |
237 | results = append(results, hostnames...)
238 | results = append(results, c.externalHostnames[ipv4.String()]...)
239 | }
240 |
241 | ipv6 := c.toIPv6(ip)
242 | if ipv6 != nil {
243 | hostnames, ok := c.resolvedHostnames[ipv6.String()]
244 | if !ok {
245 | hostnames = reverseLookup(ipv6.String())
246 |
247 | c.resolvedHostnames[ipv6.String()] = hostnames
248 | }
249 |
250 | results = append(results, hostnames...)
251 | results = append(results, c.externalHostnames[ipv6.String()]...)
252 | }
253 |
254 | return uniqueLowercase(results)
255 | }
256 |
257 | func (c *Cache) vendorByIP(ip net.IP) string {
258 | if c.macVendors == nil {
259 | return ""
260 | }
261 |
262 | return c.vendorByMAC(c.toMAC(ip))
263 | }
264 |
265 | func (c *Cache) vendorByMAC(mac net.HardwareAddr) string {
266 | if mac == nil {
267 | return ""
268 | }
269 |
270 | macStr := strings.ToUpper(mac.String())
271 |
272 | for _, prefixSize := range c.macPrefixSizes {
273 | vendor, ok := c.macVendors[macStr[:prefixSize]]
274 | if ok {
275 | return vendor
276 | }
277 | }
278 |
279 | return ""
280 | }
281 |
282 | func uniqueLowercase(input []string) []string {
283 | present := map[string]bool{}
284 |
285 | unique := []string{}
286 |
287 | for _, element := range input {
288 | lowercaseElement := strings.ToLower(element)
289 |
290 | if !present[lowercaseElement] {
291 | present[lowercaseElement] = true
292 |
293 | unique = append(unique, lowercaseElement)
294 | }
295 | }
296 |
297 | return unique
298 | }
299 |
300 | func appendUnique(oldElements []string, newElements ...string) []string {
301 | present := map[string]bool{}
302 |
303 | for _, oldElement := range oldElements {
304 | present[oldElement] = true
305 | }
306 |
307 | for _, el := range newElements {
308 | _, ok := present[el]
309 | if !ok {
310 | oldElements = append(oldElements, el)
311 | present[el] = true
312 | }
313 | }
314 |
315 | return oldElements
316 | }
317 |
318 | const dnsTimeout = 200 * time.Millisecond
319 |
320 | func reverseLookup(addr string) []string {
321 | if addr == "" {
322 | return nil
323 | }
324 |
325 | resultChannel := make(chan []string)
326 |
327 | go func() {
328 | defer close(resultChannel)
329 |
330 | addrs, err := net.LookupAddr(addr)
331 | if err != nil {
332 | resultChannel <- nil
333 | }
334 |
335 | resultChannel <- addrs
336 | }()
337 |
338 | timer := time.NewTimer(dnsTimeout)
339 |
340 | select {
341 | case result := <-resultChannel:
342 | if !timer.Stop() {
343 | <-timer.C
344 | }
345 |
346 | return trimRightSlice(result, ".")
347 | case <-timer.C:
348 | return nil
349 | }
350 | }
351 |
352 | func lookup(hostname string) []net.IP {
353 | if hostname == "" {
354 | return nil
355 | }
356 |
357 | resultChannel := make(chan []net.IP)
358 |
359 | go func() {
360 | defer close(resultChannel)
361 |
362 | addrs, err := net.LookupIP(hostname)
363 | if err != nil {
364 | resultChannel <- nil
365 | }
366 |
367 | resultChannel <- addrs
368 | }()
369 |
370 | timer := time.NewTimer(dnsTimeout)
371 |
372 | select {
373 | case result := <-resultChannel:
374 | if !timer.Stop() {
375 | <-timer.C
376 | }
377 |
378 | return result
379 | case <-timer.C:
380 | return nil
381 | }
382 | }
383 |
384 | func trimRightSlice(stringSlice []string, cutset string) []string {
385 | result := make([]string, 0, len(stringSlice))
386 |
387 | for _, element := range stringSlice {
388 | result = append(result, strings.TrimRight(element, cutset))
389 | }
390 |
391 | return result
392 | }
393 |
394 | // HostInfos returns the string representations of all available infos for the
395 | // host.
396 | func (c *Cache) HostInfos(ip net.IP) []string {
397 | c.Lock()
398 | defer c.Unlock()
399 |
400 | var infos []string
401 |
402 | if ip.To4() == nil {
403 | ipv4 := c.toIPv4(ip)
404 | if ipv4 != nil {
405 | infos = append(infos, ipv4.String())
406 | }
407 | }
408 |
409 | hostnames := c.hostnames(ip)
410 |
411 | if len(hostnames) == 0 {
412 | // add MAC vendor as replacement for missing hostnames
413 | vendor := c.vendorByIP(ip)
414 | if vendor != "" {
415 | infos = append(infos, vendor)
416 | }
417 | }
418 |
419 | return append(hostnames, infos...)
420 | }
421 |
422 | func extractMAC(ip net.IP) net.HardwareAddr {
423 | if ip[11] != 0xff || ip[12] != 0xfe {
424 | return getMACFromOS(ip)
425 | }
426 |
427 | mac := make([]byte, 0, 6) //nolint:gomnd
428 |
429 | // remove ff:fe from the middle
430 | mac = append(mac, ip[8:11]...)
431 | mac = append(mac, ip[13:]...)
432 |
433 | // invert bit in first octet
434 | mac[0] ^= 2
435 |
436 | return mac
437 | }
438 |
439 | func commandAvailable(executable string, goos string) bool {
440 | if goos != "" && runtime.GOOS != goos {
441 | return false
442 | }
443 |
444 | _, err := exec.LookPath(executable)
445 |
446 | return err == nil
447 | }
448 |
449 | func fileAvailable(filename string, goos string) bool {
450 | if goos != "" && runtime.GOOS != goos {
451 | return false
452 | }
453 |
454 | _, err := os.Stat(filename)
455 |
456 | return err == nil
457 | }
458 |
459 | func readOutput(timeout time.Duration, name string, args ...string) []byte {
460 | ctx, cancel := context.WithTimeout(context.Background(), timeout)
461 | defer cancel()
462 |
463 | cmd := exec.CommandContext(ctx, name, args...)
464 |
465 | output, err := cmd.Output()
466 | if err != nil {
467 | return nil
468 | }
469 |
470 | return output
471 | }
472 |
473 | func readFileIfPossible(filename string) []byte {
474 | content, _ := os.ReadFile(filename) //nolint:gosec
475 |
476 | return content
477 | }
478 |
479 | func sortedHighToLow(m map[int]struct{}) []int {
480 | keys := make([]int, 0, len(m))
481 |
482 | for key := range m {
483 | keys = append(keys, key)
484 | }
485 |
486 | sort.Sort(sort.Reverse(sort.IntSlice(keys)))
487 |
488 | return keys
489 | }
490 |
--------------------------------------------------------------------------------
/filter_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net"
5 | "strconv"
6 | "testing"
7 |
8 | "github.com/miekg/dns"
9 | )
10 |
11 | func TestFilterNameResolutionQuery(t *testing.T) { //nolint:maintidx
12 | someIP := mustParseIP(t, "10.1.2.3")
13 | relayIPv4 := mustParseIP(t, "10.0.0.1")
14 | relayIPv6 := mustParseIP(t, "fe80::1")
15 |
16 | testCases := []struct {
17 | TestName string
18 | SpoofFor []string
19 | DontSpoofFor []string
20 | Spoof []string
21 | DontSpoof []string
22 | SpoofTypes []string
23 | DryMode bool
24 | NoRelayIPv4Configured bool
25 | NoRelayIPv6Configured bool
26 |
27 | Host string
28 | QueryType uint16 // defaults to A
29 | From net.IP
30 | FromHostnames []string
31 |
32 | ShouldRespond bool
33 | }{
34 | {
35 | TestName: "regular",
36 | Host: "foo",
37 | From: someIP,
38 | ShouldRespond: true,
39 | },
40 | {
41 | TestName: "istap",
42 | Host: isatapHostname,
43 | From: someIP,
44 | ShouldRespond: false,
45 | },
46 | {
47 | TestName: "dry",
48 | Host: "foo",
49 | DryMode: true,
50 | From: someIP,
51 | ShouldRespond: false,
52 | },
53 | {
54 | Host: "foo",
55 | Spoof: []string{"foo"},
56 | SpoofFor: []string{someIP.String()},
57 | DryMode: true,
58 | From: someIP,
59 | ShouldRespond: false,
60 | },
61 | {
62 | Host: "foo",
63 | Spoof: []string{"foo", "oof"},
64 | From: someIP,
65 | ShouldRespond: true,
66 | },
67 | {
68 | Host: "oof",
69 | Spoof: []string{"foo", "oof"},
70 | From: someIP,
71 | ShouldRespond: true,
72 | },
73 | {
74 | Host: "test",
75 | SpoofFor: []string{"anotherhost"},
76 | From: someIP,
77 | FromHostnames: []string{"anotherhost", "test"},
78 | ShouldRespond: true,
79 | },
80 | {
81 | Host: "test",
82 | DontSpoofFor: []string{"anotherhost"},
83 | From: someIP,
84 | FromHostnames: []string{"anotherhost", "test"},
85 | ShouldRespond: false,
86 | },
87 | {
88 | Host: "test",
89 | SpoofFor: []string{".anotherhost"},
90 | From: someIP,
91 | FromHostnames: []string{"foo.anotherhost", "test"},
92 | ShouldRespond: true,
93 | },
94 | {
95 | Host: "bar",
96 | Spoof: []string{"foo", "oof"},
97 | From: someIP,
98 | ShouldRespond: false,
99 | },
100 | {
101 | Host: "foo",
102 | DontSpoof: []string{"foo", "oof"},
103 | From: someIP,
104 | ShouldRespond: false,
105 | },
106 | {
107 | Host: "oof",
108 | DontSpoof: []string{"foo", "oof"},
109 | From: someIP,
110 | ShouldRespond: false,
111 | },
112 | {
113 | Host: "bar",
114 | SpoofFor: []string{"somehost"}, // resolves to 192.168.0.5
115 | From: mustParseIP(t, "192.168.0.5"),
116 | ShouldRespond: true,
117 | },
118 | {
119 | Host: "bar",
120 | SpoofFor: []string{"192.168.0.5"},
121 | From: mustParseIP(t, "192.168.0.5"),
122 | ShouldRespond: true,
123 | },
124 | {
125 | Host: "bar",
126 | DontSpoofFor: []string{"somehost"}, // resolves to 192.168.0.5
127 | From: mustParseIP(t, "192.168.0.5"),
128 | ShouldRespond: false,
129 | },
130 | {
131 | Host: "bar",
132 | SpoofFor: []string{"x"},
133 | From: someIP,
134 | ShouldRespond: false,
135 | },
136 | {
137 | Host: "foo.bar",
138 | Spoof: []string{"bar"},
139 | From: someIP,
140 | ShouldRespond: false,
141 | },
142 | {
143 | Host: "foo.bar",
144 | Spoof: []string{".bar"},
145 | From: someIP,
146 | ShouldRespond: true,
147 | },
148 | {
149 | Host: "foo.bar",
150 | DontSpoof: []string{"bar"},
151 | From: someIP,
152 | ShouldRespond: true,
153 | },
154 | {
155 | Host: "foo.bar",
156 | DontSpoof: []string{".bar"},
157 | From: someIP,
158 | ShouldRespond: false,
159 | },
160 | {
161 | Host: "foobar",
162 | DontSpoof: []string{"bar"},
163 | From: someIP,
164 | ShouldRespond: true,
165 | },
166 | {
167 | Host: "foobar",
168 | DontSpoof: []string{".bar"},
169 | From: someIP,
170 | ShouldRespond: false,
171 | },
172 | {
173 | Host: "bar",
174 | DontSpoof: []string{".bar"},
175 | From: someIP,
176 | ShouldRespond: false,
177 | },
178 | {
179 | Host: "bar",
180 | Spoof: []string{"bar."},
181 | From: someIP,
182 | ShouldRespond: true,
183 | },
184 | {
185 | Host: "bar.",
186 | Spoof: []string{"bar"},
187 | From: someIP,
188 | ShouldRespond: true,
189 | },
190 | {
191 | Host: "bar.",
192 | Spoof: []string{"bar."},
193 | From: someIP,
194 | ShouldRespond: true,
195 | },
196 | {
197 | Host: "test",
198 | QueryType: dns.TypeA,
199 | From: someIP,
200 | SpoofTypes: []string{"a"},
201 | ShouldRespond: true,
202 | },
203 | {
204 | Host: "test",
205 | QueryType: dns.TypeA,
206 | From: someIP,
207 | SpoofTypes: []string{"A"},
208 | ShouldRespond: true,
209 | },
210 | {
211 | Host: "test",
212 | QueryType: dns.TypeA,
213 | From: someIP,
214 | SpoofTypes: []string{"SOA"},
215 | ShouldRespond: false,
216 | },
217 | {
218 | Host: "test",
219 | QueryType: dns.TypeSOA,
220 | From: someIP,
221 | SpoofTypes: []string{"A", "AAAA", "SOA"},
222 | ShouldRespond: true,
223 | },
224 | {
225 | Host: "test",
226 | QueryType: dns.TypeSOA,
227 | From: someIP,
228 | SpoofTypes: []string{"A", "AAAA"},
229 | ShouldRespond: false,
230 | },
231 | {
232 | TestName: "NetBIOS unaffected by spoof-types",
233 | Host: "test",
234 | QueryType: typeNetBios,
235 | From: someIP,
236 | SpoofTypes: []string{"A", "AAAA"},
237 | ShouldRespond: true,
238 | },
239 | {
240 | TestName: "no relay IPv4",
241 | Host: "test",
242 | QueryType: dns.TypeA,
243 | From: someIP,
244 | NoRelayIPv4Configured: true,
245 | ShouldRespond: false,
246 | },
247 | {
248 | TestName: "no relay IPv6",
249 | Host: "test",
250 | QueryType: dns.TypeAAAA,
251 | From: someIP,
252 | NoRelayIPv6Configured: true,
253 | ShouldRespond: false,
254 | },
255 | }
256 |
257 | hostMatcherLookupFunction = func(host string) ([]net.IP, error) {
258 | switch host {
259 | case "somehost":
260 | return []net.IP{mustParseIP(t, "192.168.0.5")}, nil
261 | default:
262 | return nil, nil
263 | }
264 | }
265 |
266 | for i, testCase := range testCases {
267 | testCase := testCase
268 |
269 | testName := testCase.TestName
270 | if testName == "" {
271 | testName = strconv.Itoa(i)
272 | }
273 |
274 | t.Run("test_"+testName, func(t *testing.T) {
275 | types, err := parseSpoofTypes(testCase.SpoofTypes)
276 | if err != nil {
277 | t.Fatalf("parse spoof types: %v", err)
278 | }
279 |
280 | cfg := Config{
281 | SpoofFor: asHostMatchers(testCase.SpoofFor),
282 | DontSpoofFor: asHostMatchers(testCase.DontSpoofFor),
283 | Spoof: testCase.Spoof,
284 | DontSpoof: testCase.DontSpoof,
285 | DryMode: testCase.DryMode,
286 | SpoofTypes: types,
287 | }
288 |
289 | switch {
290 | case !testCase.NoRelayIPv4Configured:
291 | cfg.RelayIPv4 = relayIPv4
292 | case !testCase.NoRelayIPv6Configured:
293 | cfg.RelayIPv6 = relayIPv6
294 | }
295 |
296 | if testCase.QueryType == 0 {
297 | testCase.QueryType = dns.TypeA
298 | }
299 |
300 | shouldRespond, _ := shouldRespondToNameResolutionQuery(cfg,
301 | normalizedName(testCase.Host), testCase.QueryType, testCase.From, testCase.FromHostnames)
302 | if shouldRespond != testCase.ShouldRespond {
303 | t.Errorf("shouldRespondToNameResolutionQuery returned %v instead of %v",
304 | shouldRespond, testCase.ShouldRespond)
305 | }
306 | })
307 | }
308 | }
309 |
310 | func TestFilterDHCP(t *testing.T) {
311 | someIP := mustParseIP(t, "10.1.2.3")
312 |
313 | testCases := []struct {
314 | TestName string
315 | SpoofFor []string
316 | DontSpoofFor []string
317 | IgnoreDHCPv6NoFQDN bool
318 | DryMode bool
319 |
320 | PeerIP net.IP
321 | PeerHostnames []string
322 |
323 | ShouldRespond bool
324 | }{
325 | {
326 | TestName: "regular",
327 | PeerIP: someIP,
328 | PeerHostnames: []string{"foo"},
329 | ShouldRespond: true,
330 | },
331 | {
332 | TestName: "dry",
333 | DryMode: true,
334 | PeerIP: someIP,
335 | PeerHostnames: []string{"foo"},
336 | ShouldRespond: false,
337 | },
338 | {
339 | TestName: "dry+spooffor",
340 | SpoofFor: []string{someIP.String(), "foo"},
341 | DryMode: true,
342 | PeerIP: someIP,
343 | PeerHostnames: []string{"foo"},
344 | ShouldRespond: false,
345 | },
346 | {
347 | TestName: "fqdn",
348 | PeerIP: someIP,
349 | PeerHostnames: []string{"foo"},
350 | IgnoreDHCPv6NoFQDN: true,
351 | ShouldRespond: true,
352 | },
353 | {
354 | TestName: "ignore no fqdn",
355 | PeerIP: someIP,
356 | PeerHostnames: []string{},
357 | IgnoreDHCPv6NoFQDN: true,
358 | ShouldRespond: false,
359 | },
360 | {
361 | TestName: "spoof for ignore",
362 | SpoofFor: []string{"x"},
363 | PeerIP: someIP,
364 | PeerHostnames: []string{"foo"},
365 | ShouldRespond: false,
366 | },
367 | {
368 | TestName: "dont spoof for",
369 | DontSpoofFor: []string{"somehost"}, // resolves to 192.168.0.5
370 | PeerIP: mustParseIP(t, "192.168.0.5"),
371 | PeerHostnames: []string{"foo"},
372 | ShouldRespond: false,
373 | },
374 | {
375 | TestName: "dont spoof for",
376 | DontSpoofFor: []string{"192.168.0.5"},
377 | PeerIP: mustParseIP(t, "192.168.0.5"),
378 | PeerHostnames: []string{"foo"},
379 | ShouldRespond: false,
380 | },
381 | {
382 | TestName: "ignore whole domain",
383 | DontSpoofFor: []string{".domain"},
384 | PeerIP: someIP,
385 | PeerHostnames: []string{"test.domain"},
386 | ShouldRespond: false,
387 | },
388 | }
389 |
390 | hostMatcherLookupFunction = func(host string) ([]net.IP, error) {
391 | switch host {
392 | case "somehost":
393 | return []net.IP{mustParseIP(t, "192.168.0.5")}, nil
394 | default:
395 | return nil, nil
396 | }
397 | }
398 |
399 | for i, testCase := range testCases {
400 | testCase := testCase
401 |
402 | testName := testCase.TestName
403 | if testName == "" {
404 | testName = strconv.Itoa(i)
405 | }
406 |
407 | t.Run("test_"+testName, func(t *testing.T) {
408 | cfg := Config{
409 | SpoofFor: asHostMatchers(testCase.SpoofFor),
410 | DontSpoofFor: asHostMatchers(testCase.DontSpoofFor),
411 | DryMode: testCase.DryMode,
412 | IgnoreDHCPv6NoFQDN: testCase.IgnoreDHCPv6NoFQDN,
413 | }
414 |
415 | shouldRespond, _ := shouldRespondToDHCP(cfg,
416 | peerInfo{IP: testCase.PeerIP, Hostnames: testCase.PeerHostnames})
417 | if shouldRespond != testCase.ShouldRespond {
418 | t.Errorf("shouldRespondToDHCP returned %v instead of %v",
419 | shouldRespond, testCase.ShouldRespond)
420 | }
421 | })
422 | }
423 | }
424 |
425 | func mustParseIP(tb testing.TB, ip string) net.IP {
426 | tb.Helper()
427 |
428 | parsedIP := net.ParseIP(ip)
429 | if parsedIP == nil {
430 | tb.Fatalf("cannot parse IP %s", ip)
431 | }
432 |
433 | return parsedIP
434 | }
435 |
--------------------------------------------------------------------------------
/local_name_resolution.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/binary"
6 | "encoding/hex"
7 | "fmt"
8 | "net"
9 | "runtime"
10 | "strings"
11 | "sync"
12 | "unicode"
13 |
14 | "golang.org/x/sync/errgroup"
15 | )
16 |
17 | const (
18 | netBIOSPort = 137
19 |
20 | mDNSMulticastIPv4 = "224.0.0.251"
21 | mDNSMulticastIPv6 = "ff02::fb"
22 | mDNSPort = 5353
23 |
24 | llmnrMulticastIPv4 = "224.0.0.252"
25 | llmnrMulticastIPv6 = "ff02::1:3"
26 | llmnrPort = 5355
27 | )
28 |
29 | // RunNetBIOSResponder creates a listener for NetBIOS name resolution requests.
30 | func RunNetBIOSResponder(ctx context.Context, logger *Logger, config Config) error { //nolint:cyclop
31 | var wg sync.WaitGroup
32 |
33 | addrs, err := config.Interface.Addrs()
34 | if err != nil {
35 | return fmt.Errorf("listing addresses on interface %q: %w", config.Interface.Name, err)
36 | }
37 |
38 | activeListenAddresses := map[string]bool{}
39 |
40 | for _, addr := range addrs {
41 | ip, ok := addr.(*net.IPNet)
42 | if !ok {
43 | return fmt.Errorf("cannot extract IP address from network")
44 | }
45 |
46 | if ip.IP.To4() == nil {
47 | continue
48 | }
49 |
50 | listenIP, err := subnetBroadcastListenIP(ip)
51 | if err != nil {
52 | return fmt.Errorf("calculate subnet broadcast IP: %w", err)
53 | }
54 |
55 | if activeListenAddresses[listenIP.String()] {
56 | continue
57 | }
58 |
59 | listenAddr := &net.UDPAddr{IP: listenIP, Port: netBIOSPort}
60 |
61 | conn, err := net.ListenUDP("udp4", listenAddr)
62 | if err != nil {
63 | errStr := err.Error()
64 | if runtime.GOOS == osWindows && strings.Contains(err.Error(), "Only one usage of each socket address") {
65 | errStr += " (try disabling NetBIOS: Interface Status->"
66 | errStr += "Properties->TCP/IPv4->Advanced->WINS->Disable NetBIOS over TCP/IP)"
67 | }
68 |
69 | return fmt.Errorf(errStr)
70 | }
71 |
72 | activeListenAddresses[listenIP.String()] = true
73 |
74 | wg.Add(1)
75 |
76 | go func() {
77 | defer wg.Done()
78 | defer conn.Close() //nolint:errcheck
79 |
80 | logger.Infof("listening via UDP on %s", listenAddr)
81 |
82 | err = RunDNSHandlerOnUDPConnection(ctx, conn, logger, config)
83 | if err != nil {
84 | logger.Errorf(err.Error())
85 | }
86 | }()
87 | }
88 |
89 | wg.Wait()
90 |
91 | return nil
92 | }
93 |
94 | // subnetBroadcastListenIP returns the IP to listen on for NetBIOS broadcasts
95 | // which is the highest IP in the subnet for Linux and the regular IP for
96 | // Windows.
97 | func subnetBroadcastListenIP(ip *net.IPNet) (net.IP, error) {
98 | ipv4 := ip.IP.To4()
99 | if ipv4 == nil {
100 | return nil, fmt.Errorf("invalid argument: IPv6 instead of IPv4")
101 | }
102 |
103 | if runtime.GOOS == osWindows {
104 | return ipv4, nil
105 | }
106 |
107 | rawIP := binary.BigEndian.Uint32(ipv4)
108 | rawMask := binary.BigEndian.Uint32(net.IP(ip.Mask).To4())
109 | broadcastIP := make(net.IP, len(ipv4))
110 |
111 | binary.BigEndian.PutUint32(broadcastIP, rawIP|^rawMask)
112 |
113 | return broadcastIP, nil
114 | }
115 |
116 | func decodeNetBIOSEncoding(netBIOSName string) string {
117 | netBIOSName = normalizedName(netBIOSName)
118 |
119 | if len(netBIOSName)%2 != 0 {
120 | return netBIOSName
121 | }
122 |
123 | decodedName := ""
124 |
125 | for i := 0; i < len(netBIOSName); i += 2 {
126 | higher := netBIOSName[i] - 'A'
127 | lower := netBIOSName[i+1] - 'A'
128 |
129 | full := higher<<4 | lower //nolint:gomnd
130 | decodedName += string(full)
131 | }
132 |
133 | return decodedName
134 | }
135 |
136 | func decodeNetBIOSHostname(netBIOSName string) string {
137 | decodedName := decodeNetBIOSEncoding(netBIOSName)
138 | if decodedName == "" {
139 | return ""
140 | }
141 |
142 | for {
143 | suffix := decodedName[len(decodedName)-1]
144 |
145 | if unicode.IsGraphic(rune(suffix)) {
146 | break
147 | }
148 |
149 | decodedName = strings.TrimRight(decodedName, string(suffix))
150 | }
151 |
152 | return strings.TrimSpace(decodedName)
153 | }
154 |
155 | // The following constants hold the names of the NetBIOS suffixes
156 | // (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nbte/
157 | // 6dbf0972-bb15-4f29-afeb-baaae98416ed#Appendix_A_2).
158 | const (
159 | NetBIOSSuffixWorkstationService = "Workstation Name"
160 | NetBIOSSuffixWindowsMessengerService = "Messenger Service"
161 | NetBIOSSuffixRemoteAccessServer = "Remote Access Server"
162 | NetBIOSSuffixNetDDEService = "NetDDE Service"
163 | NetBIOSSuffixFileService = "File Service"
164 | NetBIOSSuffixRemoteAccessServiceClient = "Remote Access Client"
165 | NetBIOSSuffixMSExchangeServerInterchange = "MS Exchange Service Interchange"
166 | NetBIOSSuffixMSExchangeStore = "MS Exchange Store"
167 | NetBIOSSuffixMSExchangeDirectory = "MS Exchange Directory"
168 | NetBIOSSuffixLotusNotesServerService = "Lotus Notes Server"
169 | NetBIOSSuffixLotusNotes = "Lotus Notes"
170 | NetBIOSSuffixModemSharingServerService = "Modem Sharing Server"
171 | NetBIOSSuffixModemSharingClientService = "Modem Sharing Client"
172 | NetBIOSSuffixSMSClientsRemoteControl = "SMS Clients Remote Control"
173 | NetBIOSSuffixSMSAdministratorsRemoteControlTool = "SMS Admin Remote Control Tool"
174 | NetBIOSSuffixSMSClientsRemoteChat = "SMS Clients Remote Chat"
175 | NetBIOSSuffixSMSClientsRemoteTransfer = "SMS Clients Remote Transfer"
176 | NetBIOSSuffixDECPathworksTCPIPService = "DEC Pathworks TCPIP Service"
177 | NetBIOSSuffixMacAfeeAntivirus = "McAfee Antivirus"
178 | NetBIOSSuffixMSExchangeMTA = "MS Exchange MTA"
179 | NetBIOSSuffixMSExchangeIMC = "MS Exchange IMC"
180 | NetBIOSSuffixNetworkMonitorAgent = "Network Monitor Agent"
181 | NetBIOSSuffixNetworkMonitorApplication = "Network Monitor Application"
182 | NetBIOSSuffixDomainMasterBrowser = "Primary DC"
183 | NetBIOSSuffixMasterBrowser = "Master Browser"
184 | NetBIOSSuffixDomainControllers = "Domain Controllers"
185 | NetBIOSSuffixBrowserServiceElections = "Browser Server Elections"
186 | NetBIOSSuffixMSBrowse = "MSBROWSE Master Browser"
187 | )
188 |
189 | func decodeNetBIOSSuffix(netBIOSName string) string { //nolint:gocyclo,cyclop
190 | const decodedBIOSNameSize = 16
191 |
192 | decodedName := decodeNetBIOSEncoding(netBIOSName)
193 | if len(decodedName) != decodedBIOSNameSize {
194 | return "No Suffix"
195 | }
196 |
197 | //nolint:gomnd
198 | switch suffix := decodedName[decodedBIOSNameSize-1]; suffix {
199 | case 0x00:
200 | return NetBIOSSuffixWorkstationService
201 | case 0x01:
202 | if decodedName[decodedBIOSNameSize-2] == 0x02 {
203 | return NetBIOSSuffixMSBrowse
204 | }
205 |
206 | return NetBIOSSuffixWindowsMessengerService
207 | case 0x03:
208 | return NetBIOSSuffixWindowsMessengerService
209 | case 0x06:
210 | return NetBIOSSuffixRemoteAccessServer
211 | case 0x1C:
212 | return NetBIOSSuffixDomainControllers
213 | case 0x1D:
214 | return NetBIOSSuffixMasterBrowser
215 | case 0x1E:
216 | return NetBIOSSuffixBrowserServiceElections
217 | case 0x1F:
218 | return NetBIOSSuffixNetDDEService
219 | case 0x20:
220 | return NetBIOSSuffixFileService
221 | case 0x21:
222 | return NetBIOSSuffixRemoteAccessServiceClient
223 | case 0x22:
224 | return NetBIOSSuffixMSExchangeServerInterchange
225 | case 0x23:
226 | return NetBIOSSuffixMSExchangeStore
227 | case 0x24:
228 | return NetBIOSSuffixMSExchangeDirectory
229 | case 0x2B:
230 | return NetBIOSSuffixLotusNotesServerService
231 | case 0x2F, 0x33:
232 | return NetBIOSSuffixLotusNotes
233 | case 0x30:
234 | return NetBIOSSuffixModemSharingServerService
235 | case 0x31:
236 | return NetBIOSSuffixModemSharingClientService
237 | case 0x1B:
238 | return NetBIOSSuffixDomainMasterBrowser
239 | case 0x42:
240 | return NetBIOSSuffixMacAfeeAntivirus
241 | case 0x43:
242 | return NetBIOSSuffixSMSClientsRemoteControl
243 | case 0x44:
244 | return NetBIOSSuffixSMSAdministratorsRemoteControlTool
245 | case 0x45:
246 | return NetBIOSSuffixSMSClientsRemoteChat
247 | case 0x46:
248 | return NetBIOSSuffixSMSClientsRemoteTransfer
249 | case 0x4C, 0x52:
250 | return NetBIOSSuffixDECPathworksTCPIPService
251 | case 0x87:
252 | return NetBIOSSuffixMSExchangeMTA
253 | case 0x6A:
254 | return NetBIOSSuffixMSExchangeIMC
255 | case 0xBE:
256 | return NetBIOSSuffixNetworkMonitorAgent
257 | case 0xBF:
258 | return NetBIOSSuffixNetworkMonitorApplication
259 | default:
260 | return fmt.Sprintf("Suffix 0x%02x", suffix)
261 | }
262 | }
263 |
264 | func encodeNetBIOSLocator(ip net.IP) string {
265 | return "0000" + hex.EncodeToString(ip.To4())
266 | }
267 |
268 | // RunMDNSResponder creates a listener for mDNS requests.
269 | func RunMDNSResponder(ctx context.Context, logger *Logger, config Config) error { //nolint:dupl
270 | errGroup, ctx := errgroup.WithContext(ctx)
271 |
272 | if hasIPv4Address(config.Interface) {
273 | errGroup.Go(func() error {
274 | listenAddr := &net.UDPAddr{IP: net.ParseIP(mDNSMulticastIPv4), Port: mDNSPort}
275 |
276 | conn, err := ListenUDPMulticast(config.Interface, listenAddr)
277 | if err != nil {
278 | return fmt.Errorf("listen: %w", err)
279 | }
280 |
281 | defer conn.Close() //nolint:errcheck
282 |
283 | logger.Infof("listening via UDP on %s", listenAddr)
284 |
285 | err = RunDNSHandlerOnUDPConnection(ctx, conn, logger, config)
286 | if err != nil {
287 | return err
288 | }
289 |
290 | return nil
291 | })
292 | }
293 |
294 | if hasIPv6Address(config.Interface) && !config.NoIPv6LNR {
295 | errGroup.Go(func() error {
296 | listenAddr := &net.UDPAddr{
297 | IP: net.ParseIP(mDNSMulticastIPv6),
298 | Port: mDNSPort,
299 | Zone: config.Interface.Name,
300 | }
301 |
302 | conn, err := ListenUDPMulticast(config.Interface, listenAddr)
303 | if err != nil {
304 | return fmt.Errorf("listen: %w", err)
305 | }
306 |
307 | defer conn.Close() //nolint:errcheck
308 |
309 | logger.Infof("listening via UDP on %s", listenAddr)
310 |
311 | err = RunDNSHandlerOnUDPConnection(ctx, conn, logger, config)
312 | if err != nil {
313 | return err
314 | }
315 |
316 | return nil
317 | })
318 | }
319 |
320 | return errGroup.Wait()
321 | }
322 |
323 | // RunLLMNRResponder creates a listener for LLMNR requests.
324 | func RunLLMNRResponder(ctx context.Context, logger *Logger, config Config) error { //nolint:dupl
325 | errGroup, ctx := errgroup.WithContext(ctx)
326 |
327 | if hasIPv4Address(config.Interface) {
328 | errGroup.Go(func() error {
329 | listenAddr := &net.UDPAddr{IP: net.ParseIP(llmnrMulticastIPv4), Port: llmnrPort}
330 |
331 | conn, err := ListenUDPMulticast(config.Interface, listenAddr)
332 | if err != nil {
333 | return fmt.Errorf("listen: %w", err)
334 | }
335 |
336 | defer conn.Close() //nolint:errcheck
337 |
338 | logger.Infof("listening via UDP on %s", listenAddr)
339 |
340 | err = RunDNSHandlerOnUDPConnection(ctx, conn, logger, config)
341 | if err != nil {
342 | return err
343 | }
344 |
345 | return nil
346 | })
347 | }
348 |
349 | if hasIPv6Address(config.Interface) && !config.NoIPv6LNR {
350 | errGroup.Go(func() error {
351 | listenAddr := &net.UDPAddr{
352 | IP: net.ParseIP(llmnrMulticastIPv6),
353 | Port: llmnrPort,
354 | Zone: config.Interface.Name,
355 | }
356 |
357 | conn, err := ListenUDPMulticast(config.Interface, listenAddr)
358 | if err != nil {
359 | return fmt.Errorf("listen: %w", err)
360 | }
361 |
362 | defer conn.Close() //nolint:errcheck
363 |
364 | logger.Infof("listening via UDP on %s", listenAddr)
365 |
366 | err = RunDNSHandlerOnUDPConnection(ctx, conn, logger, config)
367 | if err != nil {
368 | return err
369 | }
370 |
371 | return nil
372 | })
373 | }
374 |
375 | return errGroup.Wait()
376 | }
377 |
378 | func hasIPv6Address(iface *net.Interface) bool {
379 | addrs, err := iface.Addrs()
380 | if err != nil {
381 | return false
382 | }
383 |
384 | for _, addr := range addrs {
385 | ip, ok := addr.(*net.IPNet)
386 | if !ok {
387 | continue
388 | }
389 |
390 | if ip.IP.To4() == nil {
391 | return true
392 | }
393 | }
394 |
395 | return false
396 | }
397 |
398 | func hasIPv4Address(iface *net.Interface) bool {
399 | addrs, err := iface.Addrs()
400 | if err != nil {
401 | return false
402 | }
403 |
404 | for _, addr := range addrs {
405 | ip, ok := addr.(*net.IPNet)
406 | if !ok {
407 | continue
408 | }
409 |
410 | if ip.IP.To4() != nil {
411 | return true
412 | }
413 | }
414 |
415 | return false
416 | }
417 |
--------------------------------------------------------------------------------
/dhcpv6.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/binary"
6 | "errors"
7 | "fmt"
8 | "math/rand"
9 | "net"
10 | "strings"
11 | "time"
12 |
13 | "github.com/insomniacslk/dhcp/dhcpv6"
14 | "github.com/insomniacslk/dhcp/dhcpv6/server6"
15 | "github.com/insomniacslk/dhcp/iana"
16 | )
17 |
18 | // DHCPv6 default values.
19 | const (
20 | // The valid lifetime for the IPv6 prefix in the option, expressed in units
21 | // of seconds. A value of 0xFFFFFFFF represents infinity.
22 | dhcpv6DefaultValidLifetime = 60 * time.Second
23 |
24 | // The time at which the requesting router should contact the delegating
25 | // router from which the prefixes in the IA_PD were obtained to extend the
26 | // lifetimes of the prefixes delegated to the IA_PD; T1 is a time duration
27 | // relative to the current time expressed in units of seconds.
28 | dhcpv6T1 = 45 * time.Second
29 |
30 | // The time at which the requesting router should contact any available
31 | // delegating router to extend the lifetimes of the prefixes assigned to the
32 | // IA_PD; T2 is a time duration relative to the current time expressed in
33 | // units of seconds.
34 | dhcpv6T2 = 50 * time.Second
35 | )
36 |
37 | // dhcpv6LinkLocalPrefix is the 64-bit link local IPv6 prefix.
38 | var dhcpv6LinkLocalPrefix = []byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0}
39 |
40 | // DHCPv6Handler holds the state for of the DHCPv6 handler method Handler().
41 | type DHCPv6Handler struct {
42 | logger *Logger
43 | serverID dhcpv6.Duid
44 | config Config
45 | }
46 |
47 | // NewDHCPv6Handler returns a DHCPv6Handler.
48 | func NewDHCPv6Handler(config Config, logger *Logger) *DHCPv6Handler {
49 | return &DHCPv6Handler{
50 | logger: logger,
51 | config: config,
52 | serverID: dhcpv6.Duid{
53 | Type: dhcpv6.DUID_LL,
54 | HwType: iana.HWTypeEthernet,
55 | LinkLayerAddr: config.Interface.HardwareAddr,
56 | },
57 | }
58 | }
59 |
60 | // Handler implements a server6.Handler.
61 | func (h *DHCPv6Handler) Handler(conn net.PacketConn, peer net.Addr, m dhcpv6.DHCPv6) {
62 | err := h.handler(conn, peer, m)
63 | if err != nil {
64 | h.logger.Errorf(err.Error())
65 | }
66 | }
67 |
68 | func (h *DHCPv6Handler) handler(conn net.PacketConn, peerAddr net.Addr, m dhcpv6.DHCPv6) error {
69 | answer, err := h.createResponse(peerAddr, m)
70 | if errors.Is(err, errNoResponse) {
71 | return nil
72 | } else if err != nil {
73 | return err
74 | }
75 |
76 | _, err = conn.WriteTo(answer.ToBytes(), peerAddr)
77 | if err != nil {
78 | return fmt.Errorf("write to %s: %w", peerAddr, err)
79 | }
80 |
81 | return nil
82 | }
83 |
84 | var errNoResponse = fmt.Errorf("no response")
85 |
86 | //nolint:cyclop
87 | func (h *DHCPv6Handler) createResponse(peerAddr net.Addr, m dhcpv6.DHCPv6) (*dhcpv6.Message, error) {
88 | msg, err := m.GetInnerMessage()
89 | if err != nil {
90 | return nil, fmt.Errorf("get inner message: %w", err)
91 | }
92 |
93 | peer := newPeerInfo(peerAddr, msg)
94 |
95 | shouldRespond, reason := shouldRespondToDHCP(h.config, peer)
96 | if !shouldRespond {
97 | h.logger.IgnoreDHCP(m.Type().String(), peer, reason)
98 |
99 | return nil, errNoResponse
100 | }
101 |
102 | var answer *dhcpv6.Message
103 |
104 | switch m.Type() {
105 | case dhcpv6.MessageTypeSolicit:
106 | answer, err = h.handleSolicit(msg, peer)
107 | case dhcpv6.MessageTypeRequest, dhcpv6.MessageTypeRebind, dhcpv6.MessageTypeRenew:
108 | answer, err = h.handleRequestRebindRenew(msg, peer)
109 | case dhcpv6.MessageTypeConfirm:
110 | answer, err = h.handleConfirm(msg, peer)
111 | case dhcpv6.MessageTypeRelease:
112 | answer, err = h.handleRelease(msg, peer)
113 | case dhcpv6.MessageTypeInformationRequest:
114 | h.logger.Debugf("ignoring %s from %s", msg.Type(), peer)
115 |
116 | return nil, errNoResponse
117 | default:
118 | h.logger.Debugf("unhandled DHCP message from %s:\n%s", peer, msg.Summary())
119 |
120 | return nil, errNoResponse
121 | }
122 |
123 | if err != nil {
124 | return nil, fmt.Errorf("configure response to %T from %s: %w", msg.Type(), peer, err)
125 | }
126 |
127 | if answer == nil {
128 | return nil, fmt.Errorf("answer to %T from %s was not configured", msg.Type(), peer)
129 | }
130 |
131 | return answer, nil
132 | }
133 |
134 | func (h *DHCPv6Handler) handleSolicit(msg *dhcpv6.Message, peer peerInfo) (*dhcpv6.Message, error) {
135 | iaNA, err := extractIANA(msg)
136 | if err != nil {
137 | return nil, fmt.Errorf("extract IANA: %w", err)
138 | }
139 |
140 | ip, opts, err := h.configureResponseOpts(iaNA, msg, peer)
141 | if err != nil {
142 | return nil, fmt.Errorf("configure response options: %w", err)
143 | }
144 |
145 | answer, err := dhcpv6.NewAdvertiseFromSolicit(msg, opts...)
146 | if err != nil {
147 | return nil, fmt.Errorf("create ADVERTISE: %w", err)
148 | }
149 |
150 | h.logger.DHCP(msg.Type(), peer, ip)
151 |
152 | return answer, nil
153 | }
154 |
155 | func (h *DHCPv6Handler) handleRequestRebindRenew(msg *dhcpv6.Message, peer peerInfo) (*dhcpv6.Message, error) {
156 | iaNA, err := extractIANA(msg)
157 | if err != nil {
158 | return nil, fmt.Errorf("extract IANA: %w", err)
159 | }
160 |
161 | ip, opts, err := h.configureResponseOpts(iaNA, msg, peer)
162 | if err != nil {
163 | return nil, fmt.Errorf("configure response options: %w", err)
164 | }
165 |
166 | answer, err := dhcpv6.NewReplyFromMessage(msg, opts...)
167 | if err != nil {
168 | return nil, fmt.Errorf("create REPLY: %w", err)
169 | }
170 |
171 | h.logger.DHCP(msg.Type(), peer, ip)
172 |
173 | return answer, nil
174 | }
175 |
176 | func (h *DHCPv6Handler) handleConfirm(msg *dhcpv6.Message, peer peerInfo) (*dhcpv6.Message, error) {
177 | answer, err := dhcpv6.NewReplyFromMessage(msg,
178 | dhcpv6.WithServerID(h.serverID),
179 | dhcpv6.WithDNS(h.config.LocalIPv6),
180 | dhcpv6.WithOption(&dhcpv6.OptStatusCode{
181 | StatusCode: iana.StatusNotOnLink,
182 | StatusMessage: iana.StatusNotOnLink.String(),
183 | }))
184 | if err != nil {
185 | return nil, fmt.Errorf("create REPLY: %w", err)
186 | }
187 |
188 | h.logger.Debugf("rejecting %s from %s", msg.Type().String(), peer)
189 |
190 | return answer, nil
191 | }
192 |
193 | func (h *DHCPv6Handler) handleRelease(msg *dhcpv6.Message, peer peerInfo) (*dhcpv6.Message, error) {
194 | iaNAs, err := extractIANAs(msg)
195 | if err != nil {
196 | return nil, err
197 | }
198 |
199 | opts := []dhcpv6.Modifier{
200 | dhcpv6.WithOption(&dhcpv6.OptStatusCode{
201 | StatusCode: iana.StatusSuccess,
202 | StatusMessage: iana.StatusSuccess.String(),
203 | }),
204 | dhcpv6.WithServerID(h.serverID),
205 | }
206 |
207 | // send status NoBinding for each address
208 | for _, iaNA := range iaNAs {
209 | opts = append(opts, dhcpv6.WithOption(&dhcpv6.OptIANA{
210 | IaId: iaNA.IaId,
211 | Options: dhcpv6.IdentityOptions{
212 | Options: []dhcpv6.Option{
213 | &dhcpv6.OptStatusCode{
214 | StatusCode: iana.StatusNoBinding,
215 | StatusMessage: iana.StatusNoBinding.String(),
216 | },
217 | },
218 | },
219 | }))
220 | }
221 |
222 | answer, err := dhcpv6.NewReplyFromMessage(msg, opts...)
223 | if err != nil {
224 | return nil, fmt.Errorf("create REPLY: %w", err)
225 | }
226 |
227 | h.logger.Debugf("aggreeing to RELEASE from %s", peer)
228 |
229 | return answer, nil
230 | }
231 |
232 | // configureResponseOpts returns the IP that should be assigned based on the
233 | // request IA_NA and the modifiers to configure the response with that IP and
234 | // the DNS server configured in the DHCPv6Handler.
235 | func (h *DHCPv6Handler) configureResponseOpts(requestIANA *dhcpv6.OptIANA,
236 | msg *dhcpv6.Message, peer peerInfo,
237 | ) (net.IP, []dhcpv6.Modifier, error) {
238 | cid := msg.GetOneOption(dhcpv6.OptionClientID)
239 | if cid == nil {
240 | return nil, nil, fmt.Errorf("no client ID option from DHCPv6 message")
241 | }
242 |
243 | duid, err := dhcpv6.DuidFromBytes(cid.ToBytes())
244 | if err != nil {
245 | return nil, nil, fmt.Errorf("deserialize DUI")
246 | }
247 |
248 | var leasedIP net.IP
249 |
250 | if duid.LinkLayerAddr == nil {
251 | h.logger.Debugf("DUID does not contain link layer address")
252 |
253 | randomIP, err := generateDeterministicRandomAddress(peer.IP)
254 | if err != nil {
255 | h.logger.Debugf("could not generate deterministic address (using SLAAC IP instead): %v", err)
256 |
257 | leasedIP = peer.IP
258 | } else {
259 | leasedIP = randomIP
260 | }
261 | } else {
262 | if h.logger != nil {
263 | go h.logger.HostInfoCache.SaveMACFromIP(peer.IP, duid.LinkLayerAddr)
264 | }
265 |
266 | leasedIP = append(leasedIP, dhcpv6LinkLocalPrefix...)
267 | leasedIP = append(leasedIP, 0, 0)
268 | leasedIP = append(leasedIP, duid.LinkLayerAddr...)
269 | }
270 |
271 | // if the IP has the first bit after the prefix set, Windows won't route
272 | // queries via this IP and use the regular self-generated link-local address
273 | // instead.
274 | leasedIP[8] |= 0b10000000
275 |
276 | return leasedIP, []dhcpv6.Modifier{
277 | dhcpv6.WithServerID(h.serverID),
278 | dhcpv6.WithDNS(h.config.LocalIPv6),
279 | dhcpv6.WithOption(&dhcpv6.OptIANA{
280 | IaId: requestIANA.IaId,
281 | T1: dhcpv6T1,
282 | T2: dhcpv6T2,
283 | Options: dhcpv6.IdentityOptions{
284 | Options: []dhcpv6.Option{
285 | &dhcpv6.OptIAAddress{
286 | IPv6Addr: leasedIP,
287 | PreferredLifetime: h.config.LeaseLifetime,
288 | ValidLifetime: h.config.LeaseLifetime,
289 | },
290 | },
291 | },
292 | }),
293 | }, nil
294 | }
295 |
296 | func generateDeterministicRandomAddress(peer net.IP) (net.IP, error) {
297 | if len(peer) != net.IPv6len {
298 | return nil, fmt.Errorf("invalid length of IPv6 address: %d bytes", len(peer))
299 | }
300 |
301 | prefixLength := net.IPv6len / 2 //nolint:gomnd
302 |
303 | seed := binary.LittleEndian.Uint64(peer[prefixLength:])
304 |
305 | deterministicAddress := make([]byte, prefixLength)
306 |
307 | n, err := rand.New(rand.NewSource(int64(seed))).Read(deterministicAddress) //nolint:gosec
308 | if err != nil {
309 | return nil, err
310 | }
311 |
312 | if n != prefixLength {
313 | return nil, fmt.Errorf("read %d random bytes instead of %d", n, prefixLength)
314 | }
315 |
316 | var newIP net.IP
317 | newIP = append(newIP, dhcpv6LinkLocalPrefix...)
318 | newIP = append(newIP, deterministicAddress...)
319 |
320 | return newIP, nil
321 | }
322 |
323 | func extractIANA(innerMessage *dhcpv6.Message) (*dhcpv6.OptIANA, error) {
324 | iaNAOpt := innerMessage.GetOneOption(dhcpv6.OptionIANA)
325 | if iaNAOpt == nil {
326 | return nil, fmt.Errorf("message does not contain IANA:\n%s", innerMessage.Summary())
327 | }
328 |
329 | iaNA, ok := iaNAOpt.(*dhcpv6.OptIANA)
330 | if !ok {
331 | return nil, fmt.Errorf("unexpected type for IANA option: %T", iaNAOpt)
332 | }
333 |
334 | return iaNA, nil
335 | }
336 |
337 | func extractIANAs(innerMessage *dhcpv6.Message) ([]*dhcpv6.OptIANA, error) {
338 | iaNAOpts := innerMessage.GetOption(dhcpv6.OptionIANA)
339 | if iaNAOpts == nil {
340 | return nil, fmt.Errorf("message does not contain IANAs:\n%s", innerMessage.Summary())
341 | }
342 |
343 | iaNAs := make([]*dhcpv6.OptIANA, 0, len(iaNAOpts))
344 |
345 | for i, iaNAOpt := range iaNAOpts {
346 | iaNA, ok := iaNAOpt.(*dhcpv6.OptIANA)
347 | if !ok {
348 | return nil, fmt.Errorf("unexpected type for IANA option %d: %T", i, iaNAOpt)
349 | }
350 |
351 | iaNAs = append(iaNAs, iaNA)
352 | }
353 |
354 | return iaNAs, nil
355 | }
356 |
357 | // RunDHCPv6Server starts a DHCPv6 server which assigns a DNS server.
358 | func RunDHCPv6Server(ctx context.Context, logger *Logger, config Config) error {
359 | listenAddr := &net.UDPAddr{
360 | IP: dhcpv6.AllDHCPRelayAgentsAndServers,
361 | Port: dhcpv6.DefaultServerPort,
362 | Zone: config.Interface.Name,
363 | }
364 |
365 | dhcvpv6Handler := NewDHCPv6Handler(config, logger)
366 |
367 | conn, err := ListenUDPMulticast(config.Interface, listenAddr)
368 | if err != nil {
369 | return err
370 | }
371 |
372 | server, err := server6.NewServer(config.Interface.Name, nil, dhcvpv6Handler.Handler,
373 | server6.WithConn(conn))
374 | if err != nil {
375 | return fmt.Errorf("starting DHCPv6 server: %w", err)
376 | }
377 |
378 | go func() {
379 | <-ctx.Done()
380 |
381 | _ = server.Close()
382 | }()
383 |
384 | logger.Infof("listening via UDP on %s", listenAddr)
385 |
386 | err = server.Serve()
387 |
388 | // if the server is stopped via ctx, we suppress the resulting errors that
389 | // result from server.Close closing the connection.
390 | if ctx.Err() != nil {
391 | return nil //nolint:nilerr
392 | }
393 |
394 | return err
395 | }
396 |
397 | type peerInfo struct {
398 | IP net.IP
399 | Hostnames []string
400 | }
401 |
402 | func newPeerInfo(addr net.Addr, innerMessage *dhcpv6.Message) peerInfo {
403 | p := peerInfo{
404 | IP: addrToIP(addr),
405 | }
406 |
407 | fqdnOpt := innerMessage.GetOneOption(dhcpv6.OptionFQDN)
408 | if fqdnOpt == nil {
409 | return p
410 | }
411 |
412 | fqdn, ok := fqdnOpt.(*dhcpv6.OptFQDN)
413 | if !ok {
414 | return p
415 | }
416 |
417 | p.Hostnames = fqdn.DomainName.Labels
418 |
419 | return p
420 | }
421 |
422 | // String returns the string representation of a peerInfo.
423 | func (p peerInfo) String() string {
424 | if len(p.Hostnames) > 0 {
425 | return p.IP.String() + " (" + strings.Join(p.Hostnames, ", ") + ")"
426 | }
427 |
428 | return p.IP.String()
429 | }
430 |
431 | func addrToIP(addr net.Addr) net.IP {
432 | udpAddr, ok := addr.(*net.UDPAddr)
433 | if ok {
434 | return udpAddr.IP
435 | }
436 |
437 | addrString := addr.String()
438 |
439 | for strings.Contains(addrString, "/") || strings.Contains(addrString, "%") {
440 | addrString = strings.SplitN(addrString, "/", 2)[0] //nolint:gomnd
441 | addrString = strings.SplitN(addrString, "%", 2)[0] //nolint:gomnd
442 | }
443 |
444 | splitAddr, _, err := net.SplitHostPort(addrString)
445 | if err == nil {
446 | addrString = splitAddr
447 | }
448 |
449 | return net.ParseIP(addrString)
450 | }
451 |
--------------------------------------------------------------------------------
/cli.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net"
7 | "os"
8 | "strings"
9 | "time"
10 |
11 | "github.com/spf13/pflag"
12 | )
13 |
14 | var stdErr = os.Stderr // this is used to make stderr redirectable without side effects
15 |
16 | // Config holds the configuration.
17 | type Config struct {
18 | RelayIPv4 net.IP
19 | RelayIPv6 net.IP
20 | SOAHostname string
21 | Interface *net.Interface
22 | TTL time.Duration
23 | LeaseLifetime time.Duration
24 | RouterLifetime time.Duration
25 | LocalIPv6 net.IP
26 | RAPeriod time.Duration
27 |
28 | NoDHCPv6DNSTakeover bool
29 | NoDHCPv6 bool
30 | NoDNS bool
31 | NoRA bool
32 | NoMDNS bool
33 | NoNetBIOS bool
34 | NoLLMNR bool
35 | NoLocalNameResolution bool
36 | NoIPv6LNR bool
37 |
38 | Spoof []string
39 | DontSpoof []string
40 | SpoofFor []*hostMatcher
41 | DontSpoofFor []*hostMatcher
42 | SpoofTypes *spoofTypes
43 | IgnoreDHCPv6NoFQDN bool
44 | DryMode bool
45 |
46 | StopAfter time.Duration
47 | Verbose bool
48 | NoColor bool
49 | NoTimestamps bool
50 | LogFileName string
51 | NoHostInfo bool
52 | HideIgnored bool
53 | RedirectStderr bool
54 | ListInterfaces bool
55 |
56 | spoofFor []string
57 | dontSpoofFor []string
58 | spoofTypes []string
59 | }
60 |
61 | // PrintSummary prints a summary of some important configuration parameters.
62 | //
63 | //nolint:forbidigo
64 | func (c Config) PrintSummary() { //nolint:cyclop
65 | fmt.Printf("Listening on interface: %s\n", c.Interface.Name)
66 |
67 | if c.LogFileName != "" {
68 | fmt.Printf("Logging to file: %s\n", c.LogFileName)
69 | }
70 |
71 | if c.RelayIPv4 != nil {
72 | fmt.Printf("IPv4 relayed to: %s\n", c.RelayIPv4)
73 | }
74 |
75 | if c.RelayIPv6 != nil {
76 | fmt.Printf("IPv6 relayed to: %s\n", c.RelayIPv6)
77 | }
78 |
79 | if c.SOAHostname != "" {
80 | fmt.Printf("SOA Requests answered with: %s\n", c.SOAHostname)
81 | }
82 |
83 | switch {
84 | case c.DryMode:
85 | var raNotice string
86 |
87 | if !c.NoRA && !c.NoDHCPv6DNSTakeover && !c.NoDHCPv6 {
88 | raNotice = " (RA is still enabled)"
89 | }
90 |
91 | fmt.Println("Dry Mode: DHCPv6 and name resolution queries will not be answered" + raNotice)
92 | default:
93 | if len(c.Spoof) != 0 {
94 | fmt.Println("Answering queries for: " + strings.Join(c.Spoof, ", "))
95 | }
96 |
97 | if len(c.DontSpoof) != 0 {
98 | fmt.Println("Ignoring queries for: " + strings.Join(c.DontSpoof, ", "))
99 | }
100 |
101 | if len(c.SpoofFor) != 0 {
102 | fmt.Println("Answering queries from: " + joinHosts(c.SpoofFor, ", "))
103 | }
104 |
105 | if len(c.DontSpoofFor) != 0 {
106 | fmt.Println("Ignoring queries from: " + joinHosts(c.DontSpoofFor, ", "))
107 | }
108 |
109 | if len(c.spoofTypes) != 0 {
110 | fmt.Println("Answering only queries of type: " + strings.Join(toUpper(c.spoofTypes), ", "))
111 | }
112 | }
113 |
114 | if c.StopAfter > 0 {
115 | fmt.Printf("Pretender will automatically terminate after: %s\n", formatStopAfter(c.StopAfter))
116 | }
117 |
118 | fmt.Println()
119 | }
120 |
121 | //nolint:forbidigo,cyclop
122 | func configFromCLI() (config Config, logger *Logger, err error) {
123 | var (
124 | interfaceName string
125 | printVersion bool
126 | )
127 |
128 | pflag.StringVarP(&interfaceName, "interface", "i", defaultInterface,
129 | "Interface to bind on, supports auto-detection by IPv4 or IPv6")
130 | pflag.IPVarP(&config.RelayIPv4, "ip4", "4", defaultRelayIPv4,
131 | "Relay IPv4 address with which queries are answered, supports auto-detection by interface or IPv6")
132 | pflag.IPVarP(&config.RelayIPv6, "ip6", "6", defaultRelayIPv6,
133 | "Relay IPv6 address with which queries are answered, supports auto-detection by interface or IPv4")
134 | pflag.StringVar(&config.SOAHostname, "soa-hostname", defaultSOAHostname,
135 | "Hostname for the SOA record (useful for Kerberos relaying)")
136 |
137 | pflag.BoolVar(&config.NoDHCPv6DNSTakeover, "no-dhcp-dns", defaultNoDHCPv6DNSTakeover,
138 | "Disable DHCPv6 DNS takeover attack (DHCPv6 and DNS)")
139 | pflag.BoolVar(&config.NoDHCPv6, "no-dhcp", defaultNoDHCPv6, "Disable DHCPv6 spoofing")
140 | pflag.BoolVar(&config.NoDNS, "no-dns", defaultNoDNS, "Disable DNS spoofing")
141 | pflag.BoolVar(&config.NoRA, "no-ra", defaultNoRA, "Disable router advertisements")
142 | pflag.BoolVar(&config.NoMDNS, "no-mdns", defaultNoMDNS, "Disable mDNS spoofing")
143 | pflag.BoolVar(&config.NoNetBIOS, "no-netbios", defaultNoNetBIOS, "Disable NetBIOS-NS spoofing")
144 | pflag.BoolVar(&config.NoLLMNR, "no-llmnr", defaultNoLLMNR, "Disable LLMNR spoofing")
145 | pflag.BoolVar(&config.NoLocalNameResolution, "no-lnr", defaultNoLocalNameResolution,
146 | "Disable local name resolution spoofing (mDNS, LLMNR, NetBIOS-NS)")
147 | pflag.BoolVar(&config.NoIPv6LNR, "no-ipv6-lnr", defaultNoIPv6LNR,
148 | "Disable mDNS and LLMNR via IPv6 (useful with allowlist or blocklist)")
149 |
150 | pflag.StringSliceVar(&config.Spoof, "spoof", defaultSpoof,
151 | "Only spoof these domains, if domain starts with a dot, all subdomains with match (allowlist)")
152 | pflag.StringSliceVar(&config.DontSpoof, "dont-spoof", defaultDontSpoof,
153 | "Do not spoof these domains, if domain starts with a dot, all subdomains with match (blocklist)")
154 | pflag.StringSliceVar(&config.spoofFor, "spoof-for", defaultSpoofFor,
155 | "Only spoof DHCPv6 and name resolution for these `hosts` (allowlist of IPs or hostnames)")
156 | pflag.StringSliceVar(&config.dontSpoofFor, "dont-spoof-for", defaultDontSpoofFor,
157 | "Do not spoof DHCPv6 and name resolution for these `hosts` (blocklist of IPs or hostnames)")
158 | pflag.StringSliceVar(&config.spoofTypes, "spoof-types", defaultSpoofTypes,
159 | "Only spoof these query `types` (A, AAA, ANY, SOA, all types are spoofed if empty)")
160 | pflag.BoolVar(&config.IgnoreDHCPv6NoFQDN, "ignore-nofqdn", defaultIgnoreDHCPv6NoFQDN,
161 | "Ignore DHCPv6 messages where the client did not include its FQDN (useful with allowlist or blocklists)")
162 | pflag.BoolVar(&config.DryMode, "dry", defaultDryMode, "Do not spoof name resolution at all, only log queries")
163 |
164 | pflag.DurationVarP(&config.TTL, "ttl", "t", defaultTTL, "Time to live for name resolution responses")
165 | pflag.DurationVar(&config.LeaseLifetime, "lease-lifetime", defaultLeaseLifetime, "DHCPv6 IP lease lifetime")
166 | pflag.DurationVar(&config.RouterLifetime, "router-lifetime", defaultRARouterLifetime,
167 | "Router lifetime specified in router advertisements")
168 | pflag.DurationVar(&config.RAPeriod, "ra-period", defaultRAPeriod, "Time period between router advertisements")
169 |
170 | pflag.DurationVar(&config.StopAfter, "stop-after", defaultStopAfter, "Stop running after this duration")
171 | pflag.BoolVarP(&config.Verbose, "verbose", "v", defaultVerbose, "Print debug information")
172 | pflag.BoolVar(&config.NoColor, "no-color", defaultNoColor, "Disables output styling")
173 | pflag.BoolVar(&config.NoTimestamps, "no-timestamps", defaultNoTimestamps, "Disables timestamps in the output")
174 | pflag.StringVarP(&config.LogFileName, "log", "l", defaultLogFileName, "Log `file` name")
175 | pflag.BoolVar(&printVersion, "version", false, "Print version information")
176 | pflag.BoolVar(&config.NoHostInfo, "no-host-info", defaultNoHostInfo, "Do not gather host information")
177 | pflag.BoolVar(&config.HideIgnored, "hide-ignored", defaultHideIgnored, "Do not log ignored queries")
178 | pflag.BoolVar(&config.RedirectStderr, "redirect-stderr", defaultRedirectStderr, "Redirect stderr to stdout")
179 | pflag.BoolVar(&config.ListInterfaces, "interfaces", defaultListInterfaces,
180 | "List interfaces and their addresses (the other options have no effect, except for --no-color)")
181 |
182 | pflag.CommandLine.SortFlags = false
183 |
184 | pflag.Parse()
185 |
186 | if pflag.NArg() > 0 {
187 | fmt.Printf("%s does not take positional arguments, only the following flags\n\n", os.Args[0])
188 | pflag.PrintDefaults()
189 |
190 | os.Exit(1)
191 | }
192 |
193 | if config.RedirectStderr {
194 | stdErr = os.Stdout
195 | }
196 |
197 | if config.NoDNS && config.NoDHCPv6 {
198 | config.NoDHCPv6DNSTakeover = true
199 | }
200 |
201 | if config.NoMDNS && config.NoLLMNR && config.NoNetBIOS {
202 | config.NoLocalNameResolution = true
203 | }
204 |
205 | if config.NoLocalNameResolution {
206 | config.NoNetBIOS = true
207 | config.NoLLMNR = true
208 | config.NoMDNS = true
209 | }
210 |
211 | if printVersion {
212 | fmt.Println(longVersion())
213 | } else {
214 | fmt.Println(shortVersion())
215 | }
216 |
217 | if printVersion {
218 | os.Exit(0)
219 | }
220 |
221 | if config.ListInterfaces {
222 | err := listInterfaces(os.Stdout, config.NoColor)
223 | if err != nil {
224 | fmt.Fprintf(stdErr, "Error: %v", err)
225 |
226 | os.Exit(1)
227 | }
228 |
229 | os.Exit(0)
230 | }
231 |
232 | logger = NewLogger().WithPrefix("Setup")
233 | logger.Verbose = config.Verbose
234 | logger.NoColor = config.NoColor
235 | logger.PrintTimestamps = !config.NoTimestamps
236 | logger.HideIgnored = config.HideIgnored
237 | logger.NoHostInfo = config.NoHostInfo
238 |
239 | if config.LogFileName != "" {
240 | f, err := os.OpenFile(config.LogFileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
241 | if err != nil {
242 | return config, logger, fmt.Errorf("log file: %w", err)
243 | }
244 |
245 | logger.LogFile = f
246 | }
247 |
248 | config.Interface, err = chooseInterface(interfaceName, config.RelayIPv4, config.RelayIPv6)
249 | if err != nil {
250 | return config, logger, interfaceError{err}
251 | }
252 |
253 | var errIPv4, errIPv6 error
254 |
255 | config.RelayIPv4, errIPv4 = autoConfigureRelayIPv4(config.Interface, config.RelayIPv4, config.RelayIPv6)
256 | config.RelayIPv6, errIPv6 = autoConfigureRelayIPv6(config.Interface, config.RelayIPv4, config.RelayIPv6)
257 |
258 | if config.RelayIPv4 == nil && !config.NoNetBIOS {
259 | logger.Errorf("no relay IPv4 configured (required for NetBIOS-NS): %v", errIPv4)
260 |
261 | config.NoNetBIOS = true
262 | }
263 |
264 | if config.RelayIPv6 == nil && config.RelayIPv4 == nil {
265 | return config, logger, fmt.Errorf("no relay IP configured: %s and %s", errIPv4, errIPv6) //nolint:errorlint
266 | }
267 |
268 | config.LocalIPv6, err = getLinkLocalIPv6Address(config.Interface)
269 | if err != nil && !config.NoDHCPv6DNSTakeover {
270 | logger.Errorf("cannot detect link local IPv6 (required for DHCPv6 DNS takeover): %v", err)
271 |
272 | config.NoDHCPv6DNSTakeover = true
273 | }
274 |
275 | config.SpoofFor = asHostMatchers(config.spoofFor)
276 | config.DontSpoofFor = asHostMatchers(config.dontSpoofFor)
277 |
278 | config.SpoofTypes, err = parseSpoofTypes(config.spoofTypes)
279 | if err != nil {
280 | return config, logger, fmt.Errorf("parsing --spoof-types: %w", err)
281 | }
282 |
283 | config.PrintSummary()
284 |
285 | return config, logger, nil
286 | }
287 |
288 | func joinHosts(hosts []*hostMatcher, sep string) string {
289 | hostStrings := make([]string, 0, len(hosts))
290 |
291 | for _, ip := range hosts {
292 | hostStrings = append(hostStrings, ip.String())
293 | }
294 |
295 | return strings.Join(hostStrings, sep)
296 | }
297 |
298 | func isLocalIP(ip net.IP) bool {
299 | ifaces, err := net.Interfaces()
300 | if err != nil {
301 | return false
302 | }
303 |
304 | for _, iface := range ifaces {
305 | addrs, err := iface.Addrs()
306 | if err != nil {
307 | continue
308 | }
309 |
310 | for _, addr := range addrs {
311 | ifaceIP, ok := addr.(*net.IPNet)
312 | if !ok {
313 | continue
314 | }
315 |
316 | if net.IP.Equal(ifaceIP.IP, ip) {
317 | return true
318 | }
319 | }
320 | }
321 |
322 | return false
323 | }
324 |
325 | //nolint:cyclop
326 | func chooseInterface(interfaceName string, ipv4, ipv6 net.IP) (*net.Interface, error) {
327 | if interfaceName != "" {
328 | return net.InterfaceByName(interfaceName)
329 | }
330 |
331 | var candidateByIPv4, candidateByIPv6 *net.Interface
332 |
333 | var err error
334 |
335 | if ipv4 != nil {
336 | if ipv4.To4() == nil {
337 | return nil, fmt.Errorf("expected IPv4 address but got IPv6 address %s", ipv4)
338 | }
339 |
340 | candidateByIPv4, err = getInterfaceByIP(ipv4)
341 | if err != nil {
342 | return nil, fmt.Errorf("choose interface by IP: %w", err)
343 | }
344 | }
345 |
346 | if ipv6 != nil {
347 | if ipv6.To4() != nil {
348 | return nil, fmt.Errorf("expected IPv6 address but got IPv6 address %s", ipv6)
349 | }
350 |
351 | candidateByIPv6, err = getInterfaceByIP(ipv6)
352 | if err != nil {
353 | return nil, fmt.Errorf("choose interface by IP: %w", err)
354 | }
355 | }
356 |
357 | if ipv4 == nil && ipv6 == nil {
358 | return nil, fmt.Errorf("interface cannot be automatically detected when no relay addresses are provided")
359 | }
360 |
361 | if candidateByIPv4 != nil && candidateByIPv6 == nil {
362 | return candidateByIPv4, nil
363 | }
364 |
365 | if candidateByIPv6 != nil && candidateByIPv4 == nil {
366 | return candidateByIPv6, nil
367 | }
368 |
369 | if candidateByIPv4 == nil && candidateByIPv6 == nil {
370 | ifaces, err := net.Interfaces()
371 | if err != nil {
372 | return nil, fmt.Errorf("listing interfaces: %w", err)
373 | }
374 |
375 | ifaces = withoutLoopback(ifaces)
376 | if len(ifaces) != 0 {
377 | return nil, fmt.Errorf("no possible candidates to determine interface")
378 | }
379 |
380 | return &ifaces[0], nil
381 | }
382 |
383 | if candidateByIPv4.Name != candidateByIPv6.Name {
384 | return nil, fmt.Errorf("cannot determine interface: conflict between %s (by IPv4) and %s (by IPv6)",
385 | candidateByIPv4.Name, candidateByIPv6.Name)
386 | }
387 |
388 | return candidateByIPv4, nil
389 | }
390 |
391 | func withoutLoopback(ifaces []net.Interface) []net.Interface {
392 | filtered := []net.Interface{}
393 |
394 | for _, iface := range ifaces {
395 | if iface.Flags&net.FlagLoopback == 0 {
396 | filtered = append(filtered, iface)
397 | }
398 | }
399 |
400 | return filtered
401 | }
402 |
403 | func getInterfaceByIP(ip net.IP) (*net.Interface, error) {
404 | ifaces, err := net.Interfaces()
405 | if err != nil {
406 | return nil, fmt.Errorf("listing interfaces: %w", err)
407 | }
408 |
409 | for _, iface := range ifaces {
410 | addrs, err := iface.Addrs()
411 | if err != nil {
412 | continue
413 | }
414 |
415 | for _, addr := range addrs {
416 | ifaceIP, ok := addr.(*net.IPNet)
417 | if !ok {
418 | return nil, fmt.Errorf("unexpected IP type: %T", addr)
419 | }
420 |
421 | if net.IP.Equal(ifaceIP.IP, ip) {
422 | return &iface, nil
423 | }
424 | }
425 | }
426 |
427 | return nil, fmt.Errorf("cannot find interface with IP %s", ip)
428 | }
429 |
430 | func autoConfigureRelayIPv4(iface *net.Interface, ipv4, ipv6 net.IP) (net.IP, error) {
431 | if ipv4 == nil {
432 | if ipv6 != nil && !isLocalIP(ipv6) {
433 | return nil, fmt.Errorf("IPv4 auto detection disabled when remote IPv6 relay is configured")
434 | }
435 |
436 | ip, err := detectLocalIPv4(iface)
437 | if err != nil {
438 | return nil, fmt.Errorf("cannot auto-detect IPv4: %w", err)
439 | }
440 |
441 | return ip, nil
442 | }
443 |
444 | if ipv4.To4() == nil {
445 | return nil, fmt.Errorf("expected IPv4 address but got IPv6 address %s", ipv4)
446 | }
447 |
448 | return ipv4, nil
449 | }
450 |
451 | func detectLocalIPv4(iface *net.Interface) (net.IP, error) {
452 | addrs, err := iface.Addrs()
453 | if err != nil {
454 | return nil, fmt.Errorf("listing addresses of interface %q: %w", iface.Name, err)
455 | }
456 |
457 | candidates := []net.IP{}
458 |
459 | for _, addr := range addrs {
460 | ip, ok := addr.(*net.IPNet)
461 | if !ok {
462 | return nil, fmt.Errorf("unexpected IP type: %T", addr)
463 | }
464 |
465 | if ip.IP.To4() == nil {
466 | continue
467 | }
468 |
469 | candidates = append(candidates, ip.IP)
470 | }
471 |
472 | if len(candidates) > 1 {
473 | return nil, fmt.Errorf("multiple possible IPv4 addresses on interface %q", iface.Name)
474 | }
475 |
476 | if len(candidates) == 0 {
477 | return nil, fmt.Errorf("interface %q has no IPv4 addresses", iface.Name)
478 | }
479 |
480 | return candidates[0], nil
481 | }
482 |
483 | func autoConfigureRelayIPv6(iface *net.Interface, ipv4, ipv6 net.IP) (net.IP, error) {
484 | if ipv6 == nil {
485 | if ipv4 != nil && !isLocalIP(ipv4) {
486 | return nil, fmt.Errorf("IPv4 auto detection disabled when remote IPv4 relay is configured")
487 | }
488 |
489 | ip, err := detectLocalIPv6(iface)
490 | if err != nil {
491 | return nil, fmt.Errorf("cannot auto-detect IPv6: %w", err)
492 | }
493 |
494 | return ip, nil
495 | }
496 |
497 | if ipv6.To4() != nil {
498 | return nil, fmt.Errorf("expected IPv6 address but got IPv4 address %s", ipv6)
499 | }
500 |
501 | return ipv6, nil
502 | }
503 |
504 | func detectLocalIPv6(iface *net.Interface) (net.IP, error) {
505 | addrs, err := iface.Addrs()
506 | if err != nil {
507 | return nil, fmt.Errorf("listing addresses of interface %q: %w", iface.Name, err)
508 | }
509 |
510 | candidates := []net.IP{}
511 |
512 | for _, addr := range addrs {
513 | ip, ok := addr.(*net.IPNet)
514 | if !ok {
515 | return nil, fmt.Errorf("unexpected IP type: %T", addr)
516 | }
517 |
518 | if ip.IP.To4() != nil || ip.IP.IsLinkLocalMulticast() {
519 | continue
520 | }
521 |
522 | candidates = append(candidates, ip.IP)
523 | }
524 |
525 | if len(candidates) > 1 {
526 | return nil, fmt.Errorf("multiple possible IPv6 addresses on interface %q", iface.Name)
527 | }
528 |
529 | if len(candidates) == 0 {
530 | return nil, fmt.Errorf("interface %q has no IPv6 addresses", iface.Name)
531 | }
532 |
533 | return candidates[0], nil
534 | }
535 |
536 | func getLinkLocalIPv6Address(iface *net.Interface) (net.IP, error) {
537 | addrs, err := iface.Addrs()
538 | if err != nil {
539 | return nil, fmt.Errorf("gather addresses of interface %q: %w", iface.Name, err)
540 | }
541 |
542 | for _, addr := range addrs {
543 | ip, ok := addr.(*net.IPNet)
544 | if !ok {
545 | return nil, fmt.Errorf("unexpected IP type: %T", addr)
546 | }
547 |
548 | if ip.IP.IsLinkLocalUnicast() {
549 | return ip.IP, nil
550 | }
551 | }
552 |
553 | return nil, fmt.Errorf("interface %q has no link local IPv6 address", iface.Name)
554 | }
555 |
556 | func listInterfaces(w io.Writer, noColor bool) error {
557 | ifaces, err := net.Interfaces()
558 | if err != nil {
559 | return fmt.Errorf("listing interfaces: %w", err)
560 | }
561 |
562 | indent := " "
563 |
564 | for _, iface := range ifaces {
565 | fmt.Fprintf(w, "\n%2d: %s %s:\n", iface.Index,
566 | styled(iface.Name, noColor, bold, fgRed), styled("<"+iface.Flags.String()+">", noColor, faint))
567 |
568 | if iface.HardwareAddr != nil {
569 | fmt.Fprintf(w, "%sMAC : %s\n", indent, styled(iface.HardwareAddr.String(), noColor, bold))
570 | }
571 |
572 | addrs, err := iface.Addrs()
573 | if err != nil {
574 | return fmt.Errorf("gather addresses of interface %q: %w", iface.Name, err)
575 | }
576 |
577 | for _, addr := range addrs {
578 | ip, ok := addr.(*net.IPNet)
579 | if !ok {
580 | return fmt.Errorf("unexpected IP type: %T", addr)
581 | }
582 |
583 | ipType := "IPv6"
584 | if ip.IP.To4() != nil {
585 | ipType = "IPv4"
586 | }
587 |
588 | fmt.Fprintf(w, "%s%s: %s %s\n", indent, ipType, styled(addr.String(), noColor, bold),
589 | styled(ipProperties(ip.IP), noColor, faint))
590 | }
591 | }
592 |
593 | return nil
594 | }
595 |
596 | func ipProperties(ip net.IP) string {
597 | properties := []string{}
598 | if ip.IsLoopback() {
599 | properties = append(properties, "loopback")
600 | }
601 |
602 | if ip.IsGlobalUnicast() {
603 | properties = append(properties, "global unicast")
604 | }
605 |
606 | if ip.IsLinkLocalUnicast() {
607 | properties = append(properties, "link local unicast")
608 | }
609 |
610 | if ip.IsMulticast() {
611 | properties = append(properties, "multicast")
612 | }
613 |
614 | return "<" + strings.Join(properties, "|") + ">"
615 | }
616 |
617 | type interfaceError struct {
618 | error
619 | }
620 |
621 | func (ie interfaceError) Error() string {
622 | return ie.error.Error()
623 | }
624 |
625 | func formatStopAfter(d time.Duration) string {
626 | stopDuration := strings.TrimSuffix(strings.TrimSuffix(d.String(), "0s"), "0m")
627 | if d < time.Minute {
628 | return stopDuration
629 | }
630 |
631 | now := time.Now()
632 | stopTime := now.Add(d)
633 |
634 | dateFormatter := "03:04pm"
635 | if stopTime.Day() != now.Day() {
636 | dateFormatter += " 02-Jan-06"
637 | }
638 |
639 | return fmt.Sprintf("%s (%s)", stopDuration, stopTime.Format(dateFormatter))
640 | }
641 |
642 | func toUpper(elements []string) []string {
643 | upper := make([]string, 0, len(elements))
644 |
645 | for _, el := range elements {
646 | upper = append(upper, strings.ToUpper(el))
647 | }
648 |
649 | return upper
650 | }
651 |
--------------------------------------------------------------------------------