all: include client IP if ctrld is dnsmasq upstream

So ctrld can record the raw/original client IP instead of looking up
from MAC to IP, which may not the right choice in some network setup
like using wireguard/vpn on Merlin router.
This commit is contained in:
Cuong Manh Le
2023-09-07 11:09:53 +00:00
committed by Cuong Manh Le
parent ee5eb4fc4e
commit 0f3e8c7ada
9 changed files with 153 additions and 26 deletions
+40 -16
View File
@@ -54,8 +54,7 @@ func (p *prog) serveDNS(listenerNum string) error {
domain := canonicalName(q.Name)
reqId := requestID()
remoteIP, _, _ := net.SplitHostPort(w.RemoteAddr().String())
mac := macFromMsg(m)
ci := p.getClientInfo(remoteIP, mac)
ci := p.getClientInfo(remoteIP, m)
remoteAddr := spoofRemoteAddr(w.RemoteAddr(), ci)
fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String())
t := time.Now()
@@ -419,18 +418,24 @@ func needLocalIPv6Listener() bool {
return ctrldnet.SupportsIPv6ListenLocal() && runtime.GOOS == "windows"
}
func macFromMsg(msg *dns.Msg) string {
// ipAndMacFromMsg extracts IP and MAC information included in a DNS message, if any.
func ipAndMacFromMsg(msg *dns.Msg) (string, string) {
ip, mac := "", ""
if opt := msg.IsEdns0(); opt != nil {
for _, s := range opt.Option {
switch e := s.(type) {
case *dns.EDNS0_LOCAL:
if e.Code == EDNS0_OPTION_MAC {
return net.HardwareAddr(e.Data).String()
mac = net.HardwareAddr(e.Data).String()
}
case *dns.EDNS0_SUBNET:
if len(e.Address) > 0 && !e.Address.IsLoopback() {
ip = e.Address.String()
}
}
}
}
return ""
return ip, mac
}
func spoofRemoteAddr(addr net.Addr, ci *ctrld.ClientInfo) net.Addr {
@@ -484,19 +489,38 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha
return s, errCh
}
func (p *prog) getClientInfo(ip, mac string) *ctrld.ClientInfo {
func (p *prog) getClientInfo(remoteIP string, msg *dns.Msg) *ctrld.ClientInfo {
ci := &ctrld.ClientInfo{}
if mac != "" {
ci.Mac = mac
ci.IP = p.ciTable.LookupIP(mac)
} else {
ci.IP = ip
ci.Mac = p.ciTable.LookupMac(ip)
if ip == "127.0.0.1" || ip == "::1" {
ci.IP = p.ciTable.LookupIP(ci.Mac)
}
ci.IP, ci.Mac = ipAndMacFromMsg(msg)
switch {
case ci.IP != "" && ci.Mac != "":
// Nothing to do.
case ci.IP == "" && ci.Mac != "":
// Have MAC, no IP.
ci.IP = p.ciTable.LookupIP(ci.Mac)
case ci.IP == "" && ci.Mac == "":
// Have nothing, use remote IP then lookup MAC.
ci.IP = remoteIP
fallthrough
case ci.IP != "" && ci.Mac == "":
// Have IP, no MAC.
ci.Mac = p.ciTable.LookupMac(ci.IP)
}
// If MAC is still empty here, that mean the requests are made from virtual interface,
// like VPN/Wireguard clients, so we use whatever MAC address associated with remoteIP
// (most likely 127.0.0.1), and ci.IP as hostname, so we can distinguish those clients.
if ci.Mac == "" {
ci.Mac = p.ciTable.LookupMac(remoteIP)
if hostname := p.ciTable.LookupHostname(ci.IP, ""); hostname != "" {
ci.Hostname = hostname
} else {
ci.Hostname = ci.IP
p.ciTable.StoreVPNClient(ci)
}
} else {
ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac)
}
ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac)
return ci
}
+27 -9
View File
@@ -156,19 +156,27 @@ func TestCache(t *testing.T) {
assert.Equal(t, answer2.Rcode, got2.Rcode)
}
func Test_macFromMsg(t *testing.T) {
func Test_ipAndMacFromMsg(t *testing.T) {
tests := []struct {
name string
ip string
wantIp bool
mac string
wantMac bool
}{
{"has mac", "4c:20:b8:ab:87:1b", true},
{"no mac", "4c:20:b8:ab:87:1b", false},
{"has ip v4 and mac", "1.2.3.4", true, "4c:20:b8:ab:87:1b", true},
{"has ip v6 and mac", "2606:1a40:3::1", true, "4c:20:b8:ab:87:1b", true},
{"no ip", "1.2.3.4", false, "4c:20:b8:ab:87:1b", false},
{"no mac", "1.2.3.4", false, "4c:20:b8:ab:87:1b", false},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ip := net.ParseIP(tc.ip)
if ip == nil {
t.Fatal("missing IP")
}
hw, err := net.ParseMAC(tc.mac)
if err != nil {
t.Fatal(err)
@@ -180,13 +188,23 @@ func Test_macFromMsg(t *testing.T) {
ec1 := &dns.EDNS0_LOCAL{Code: EDNS0_OPTION_MAC, Data: hw}
o.Option = append(o.Option, ec1)
}
m.Extra = append(m.Extra, o)
got := macFromMsg(m)
if tc.wantMac && got != tc.mac {
t.Errorf("mismatch, want: %q, got: %q", tc.mac, got)
if tc.wantIp {
ec2 := &dns.EDNS0_SUBNET{Address: ip}
o.Option = append(o.Option, ec2)
}
if !tc.wantMac && got != "" {
t.Errorf("unexpected mac: %q", got)
m.Extra = append(m.Extra, o)
gotIP, gotMac := ipAndMacFromMsg(m)
if tc.wantMac && gotMac != tc.mac {
t.Errorf("mismatch, want: %q, got: %q", tc.mac, gotMac)
}
if !tc.wantMac && gotMac != "" {
t.Errorf("unexpected mac: %q", gotMac)
}
if tc.wantIp && gotIP != tc.ip {
t.Errorf("mismatch, want: %q, got: %q", tc.ip, gotIP)
}
if !tc.wantIp && gotIP != "" {
t.Errorf("unexpected ip: %q", gotIP)
}
})
}