wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
60
service/network/netutils/address.go
Normal file
60
service/network/netutils/address.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package netutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
)
|
||||
|
||||
var errInvalidIP = errors.New("invalid IP address")
|
||||
|
||||
// IPPortFromAddr extracts or parses the IP address and port contained in the given address.
|
||||
func IPPortFromAddr(addr net.Addr) (ip net.IP, port uint16, err error) {
|
||||
// Convert addr to IP if needed.
|
||||
switch v := addr.(type) {
|
||||
case *net.TCPAddr:
|
||||
return v.IP, uint16(v.Port), nil
|
||||
case *net.UDPAddr:
|
||||
return v.IP, uint16(v.Port), nil
|
||||
case *net.IPAddr:
|
||||
return v.IP, 0, nil
|
||||
case *net.UnixAddr:
|
||||
return nil, 0, errors.New("unix addresses don't have IPs")
|
||||
default:
|
||||
return ParseIPPort(addr.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ProtocolFromNetwork returns the protocol from the given net, as used in the "net" golang stdlib.
|
||||
func ProtocolFromNetwork(net string) (protocol packet.IPProtocol) {
|
||||
switch net {
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
return packet.TCP
|
||||
case "udp", "udp4", "udp6":
|
||||
return packet.UDP
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ParseIPPort parses a <ip>:port formatted address.
|
||||
func ParseIPPort(address string) (net.IP, uint16, error) {
|
||||
ipString, portString, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ipString)
|
||||
if ip == nil {
|
||||
return nil, 0, errInvalidIP
|
||||
}
|
||||
|
||||
port, err := strconv.ParseUint(portString, 10, 16)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return ip, uint16(port), nil
|
||||
}
|
||||
99
service/network/netutils/dns.go
Normal file
99
service/network/netutils/dns.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package netutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
cleanDomainRegex = regexp.MustCompile(
|
||||
`^` + // match beginning
|
||||
`(` + // start subdomain group
|
||||
`(xn--)?` + // idn prefix
|
||||
`[a-z0-9_-]{1,63}` + // main chunk
|
||||
`\.` + // ending with a dot
|
||||
`)*` + // end subdomain group, allow any number of subdomains
|
||||
`(xn--)?` + // TLD idn prefix
|
||||
`[a-z0-9_-]{1,63}` + // TLD main chunk with at least one character (for custom ones)
|
||||
`\.` + // ending with a dot
|
||||
`$`, // match end
|
||||
)
|
||||
|
||||
// dnsSDDomainRegex is a lot more lax to better suit the allowed characters in DNS-SD.
|
||||
// Not all characters have been allowed - some special characters were
|
||||
// removed to reduce the general attack surface.
|
||||
dnsSDDomainRegex = regexp.MustCompile(
|
||||
// Start of charset selection.
|
||||
`^[` +
|
||||
// Printable ASCII (character code 32-127), excluding some special characters.
|
||||
` !#$%&()*+,\-\./0-9:;=?@A-Z[\\\]^_\a-z{|}~` +
|
||||
// Only latin characters from extended ASCII (character code 128-255).
|
||||
`ŠŒŽšœžŸ¡¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ` +
|
||||
// End of charset selection.
|
||||
`]*$`,
|
||||
)
|
||||
)
|
||||
|
||||
// IsValidFqdn returns whether the given string is a valid fqdn.
|
||||
func IsValidFqdn(fqdn string) bool {
|
||||
// root zone
|
||||
if fqdn == "." {
|
||||
return true
|
||||
}
|
||||
|
||||
// check max length
|
||||
if len(fqdn) > 256 {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsFqdn checks if a domain name is fully qualified.
|
||||
if !dns.IsFqdn(fqdn) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Use special check for .local domains to support DNS-SD.
|
||||
if strings.HasSuffix(fqdn, ".local.") {
|
||||
return dnsSDDomainRegex.MatchString(fqdn)
|
||||
}
|
||||
|
||||
// check with regex
|
||||
if !cleanDomainRegex.MatchString(fqdn) {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsDomainName checks if s is a valid domain name, it returns the number of
|
||||
// labels and true, when a domain name is valid. Note that non fully qualified
|
||||
// domain name is considered valid, in this case the last label is counted in
|
||||
// the number of labels. When false is returned the number of labels is not
|
||||
// defined. Also note that this function is extremely liberal; almost any
|
||||
// string is a valid domain name as the DNS is 8 bit protocol. It checks if each
|
||||
// label fits in 63 characters and that the entire name will fit into the 255
|
||||
// octet wire format limit.
|
||||
_, ok := dns.IsDomainName(fqdn)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IPsToRRs transforms the given IPs to resource records.
|
||||
func IPsToRRs(domain string, ips []net.IP) ([]dns.RR, error) {
|
||||
records := make([]dns.RR, 0, len(ips))
|
||||
var rr dns.RR
|
||||
var err error
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
rr, err = dns.NewRR(fmt.Sprintf("%s 17 IN A %s", domain, ip))
|
||||
} else {
|
||||
rr, err = dns.NewRR(fmt.Sprintf("%s 17 IN AAAA %s", domain, ip))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create record for %s: %w", ip, err)
|
||||
}
|
||||
records = append(records, rr)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
47
service/network/netutils/dns_test.go
Normal file
47
service/network/netutils/dns_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package netutils
|
||||
|
||||
import "testing"
|
||||
|
||||
func testDomainValidity(t *testing.T, domain string, isValid bool) {
|
||||
t.Helper()
|
||||
|
||||
if IsValidFqdn(domain) != isValid {
|
||||
t.Errorf("domain %s failed check: was valid=%v, expected valid=%v", domain, IsValidFqdn(domain), isValid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// valid
|
||||
testDomainValidity(t, ".", true)
|
||||
testDomainValidity(t, "at.", true)
|
||||
testDomainValidity(t, "orf.at.", true)
|
||||
testDomainValidity(t, "www.orf.at.", true)
|
||||
testDomainValidity(t, "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.x.y.z.example.org.", true)
|
||||
testDomainValidity(t, "a_a.com.", true)
|
||||
testDomainValidity(t, "a-a.com.", true)
|
||||
testDomainValidity(t, "a_a.com.", true)
|
||||
testDomainValidity(t, "a-a.com.", true)
|
||||
testDomainValidity(t, "xn--a.com.", true)
|
||||
testDomainValidity(t, "xn--asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasd.com.", true)
|
||||
|
||||
// maybe valid
|
||||
testDomainValidity(t, "-.com.", true)
|
||||
testDomainValidity(t, "_.com.", true)
|
||||
testDomainValidity(t, "a_.com.", true)
|
||||
testDomainValidity(t, "a-.com.", true)
|
||||
testDomainValidity(t, "_a.com.", true)
|
||||
testDomainValidity(t, "-a.com.", true)
|
||||
|
||||
// invalid
|
||||
testDomainValidity(t, ".com.", false)
|
||||
testDomainValidity(t, ".com.", false)
|
||||
testDomainValidity(t, "xn--asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf.com.", false)
|
||||
testDomainValidity(t, "asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf.com.", false)
|
||||
testDomainValidity(t, "asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdf.com.", false)
|
||||
testDomainValidity(t, "asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.asdf.as.com.", false)
|
||||
|
||||
// real world examples
|
||||
testDomainValidity(t, "iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com.", true)
|
||||
}
|
||||
160
service/network/netutils/ip.go
Normal file
160
service/network/netutils/ip.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package netutils
|
||||
|
||||
import "net"
|
||||
|
||||
// IPScope is the scope of the IP address.
|
||||
type IPScope int8
|
||||
|
||||
// Defined IP Scopes.
|
||||
const (
|
||||
Invalid IPScope = iota - 1
|
||||
Undefined
|
||||
HostLocal
|
||||
LinkLocal
|
||||
SiteLocal
|
||||
Global
|
||||
LocalMulticast
|
||||
GlobalMulticast
|
||||
)
|
||||
|
||||
// ClassifyIP returns the network scope of the given IP address.
|
||||
// Deprecated: Please use the new GetIPScope instead.
|
||||
func ClassifyIP(ip net.IP) IPScope {
|
||||
return GetIPScope(ip)
|
||||
}
|
||||
|
||||
// GetIPScope returns the network scope of the given IP address.
|
||||
func GetIPScope(ip net.IP) IPScope { //nolint:gocognit
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
// IPv4
|
||||
switch {
|
||||
case ip4[0] == 0 && ip4[1] == 0 && ip4[2] == 0 && ip4[3] == 0:
|
||||
// 0.0.0.0/32
|
||||
return LocalMulticast // Used as source for L2 based protocols with no L3 addressing.
|
||||
case ip4[0] == 0:
|
||||
// 0.0.0.0/8
|
||||
return Invalid
|
||||
case ip4[0] == 10:
|
||||
// 10.0.0.0/8 (RFC1918)
|
||||
return SiteLocal
|
||||
case ip4[0] == 100 && ip4[1]&0b11000000 == 64:
|
||||
// 100.64.0.0/10 (RFC6598)
|
||||
return SiteLocal
|
||||
case ip4[0] == 127:
|
||||
// 127.0.0.0/8 (RFC1918)
|
||||
return HostLocal
|
||||
case ip4[0] == 169 && ip4[1] == 254:
|
||||
// 169.254.0.0/16 (RFC3927)
|
||||
return LinkLocal
|
||||
case ip4[0] == 172 && ip4[1]&0b11110000 == 16:
|
||||
// 172.16.0.0/12 (RFC1918)
|
||||
return SiteLocal
|
||||
case ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 2:
|
||||
// 192.0.2.0/24 (TEST-NET-1, RFC5737)
|
||||
return Invalid
|
||||
case ip4[0] == 192 && ip4[1] == 168:
|
||||
// 192.168.0.0/16 (RFC1918)
|
||||
return SiteLocal
|
||||
case ip4[0] == 198 && ip4[1] == 51 && ip4[2] == 100:
|
||||
// 198.51.100.0/24 (TEST-NET-2, RFC5737)
|
||||
return Invalid
|
||||
case ip4[0] == 203 && ip4[1] == 0 && ip4[2] == 113:
|
||||
// 203.0.113.0/24 (TEST-NET-3, RFC5737)
|
||||
return Invalid
|
||||
case ip4[0] == 224:
|
||||
// 224.0.0.0/8 (RFC5771)
|
||||
return LocalMulticast
|
||||
case ip4[0] == 233 && ip4[1] == 252 && ip4[2] == 0:
|
||||
// 233.252.0.0/24 (MCAST-TEST-NET; RFC5771, RFC6676)
|
||||
return Invalid
|
||||
case ip4[0] >= 225 && ip4[0] <= 238:
|
||||
// 225.0.0.0/8 - 238.0.0.0/8 (RFC5771)
|
||||
return GlobalMulticast
|
||||
case ip4[0] == 239:
|
||||
// 239.0.0.0/8 (RFC2365)
|
||||
return LocalMulticast
|
||||
case ip4[0] == 255 && ip4[1] == 255 && ip4[2] == 255 && ip4[3] == 255:
|
||||
// 255.255.255.255/32
|
||||
return LocalMulticast
|
||||
case ip4[0] >= 240:
|
||||
// 240.0.0.0/8 - 255.0.0.0/8 (minus 255.255.255.255/32)
|
||||
return Invalid
|
||||
default:
|
||||
return Global
|
||||
}
|
||||
} else if len(ip) == net.IPv6len {
|
||||
// IPv6
|
||||
switch {
|
||||
case ip.Equal(net.IPv6zero):
|
||||
return Invalid
|
||||
case ip.Equal(net.IPv6loopback):
|
||||
return HostLocal
|
||||
case ip[0]&0xfe == 0xfc:
|
||||
// fc00::/7
|
||||
return SiteLocal
|
||||
case ip[0] == 0xfe && ip[1]&0xc0 == 0x80:
|
||||
// fe80::/10
|
||||
return LinkLocal
|
||||
case ip[0] == 0xff && ip[1] <= 0x05:
|
||||
// ff00::/16 - ff05::/16
|
||||
return LocalMulticast
|
||||
case ip[0] == 0xff:
|
||||
// other ff00::/8
|
||||
return GlobalMulticast
|
||||
default:
|
||||
return Global
|
||||
}
|
||||
}
|
||||
return Invalid
|
||||
}
|
||||
|
||||
// IsLocalhost returns whether the IP refers to the host itself.
|
||||
func (scope IPScope) IsLocalhost() bool {
|
||||
return scope == HostLocal
|
||||
}
|
||||
|
||||
// IsLAN returns true if the scope is site-local or link-local.
|
||||
func (scope IPScope) IsLAN() bool {
|
||||
switch scope { //nolint:exhaustive // Looking for something specific.
|
||||
case SiteLocal, LinkLocal, LocalMulticast:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsGlobal returns true if the scope is global.
|
||||
func (scope IPScope) IsGlobal() bool {
|
||||
switch scope { //nolint:exhaustive // Looking for something specific.
|
||||
case Global, GlobalMulticast:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetBroadcastAddress returns the broadcast address of the given IP and network mask.
|
||||
// If a mixed IPv4/IPv6 input is given, it returns nil.
|
||||
func GetBroadcastAddress(ip net.IP, netMask net.IPMask) net.IP {
|
||||
// Convert to standard v4.
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
ip = ip4
|
||||
}
|
||||
mask := net.IP(netMask)
|
||||
if ip4Mask := mask.To4(); ip4Mask != nil {
|
||||
mask = ip4Mask
|
||||
}
|
||||
|
||||
// Check for mixed v4/v6 input.
|
||||
if len(ip) != len(mask) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge to broadcast address
|
||||
n := len(ip)
|
||||
broadcastAddress := make(net.IP, n)
|
||||
for i := 0; i < n; i++ {
|
||||
broadcastAddress[i] = ip[i] | ^mask[i]
|
||||
}
|
||||
return broadcastAddress
|
||||
}
|
||||
51
service/network/netutils/ip_test.go
Normal file
51
service/network/netutils/ip_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package netutils
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIPScope(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testScope(t, net.IPv4(71, 87, 113, 211), Global)
|
||||
testScope(t, net.IPv4(127, 0, 0, 1), HostLocal)
|
||||
testScope(t, net.IPv4(127, 255, 255, 1), HostLocal)
|
||||
testScope(t, net.IPv4(192, 168, 172, 24), SiteLocal)
|
||||
testScope(t, net.IPv4(172, 15, 1, 1), Global)
|
||||
testScope(t, net.IPv4(172, 16, 1, 1), SiteLocal)
|
||||
testScope(t, net.IPv4(172, 31, 1, 1), SiteLocal)
|
||||
testScope(t, net.IPv4(172, 32, 1, 1), Global)
|
||||
}
|
||||
|
||||
func testScope(t *testing.T, ip net.IP, expectedScope IPScope) {
|
||||
t.Helper()
|
||||
|
||||
c := GetIPScope(ip)
|
||||
if c != expectedScope {
|
||||
t.Errorf("%s is %s, expected %s", ip, scopeName(c), scopeName(expectedScope))
|
||||
}
|
||||
}
|
||||
|
||||
func scopeName(c IPScope) string {
|
||||
switch c {
|
||||
case Invalid:
|
||||
return "invalid"
|
||||
case Undefined:
|
||||
return "undefined"
|
||||
case HostLocal:
|
||||
return "hostLocal"
|
||||
case LinkLocal:
|
||||
return "linkLocal"
|
||||
case SiteLocal:
|
||||
return "siteLocal"
|
||||
case Global:
|
||||
return "global"
|
||||
case LocalMulticast:
|
||||
return "localMulticast"
|
||||
case GlobalMulticast:
|
||||
return "globalMulticast"
|
||||
default:
|
||||
return "undefined"
|
||||
}
|
||||
}
|
||||
51
service/network/netutils/tcpassembly.go
Normal file
51
service/network/netutils/tcpassembly.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package netutils
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/tcpassembly"
|
||||
)
|
||||
|
||||
// SimpleStreamAssemblerManager is a simple manager for github.com/google/gopacket/tcpassembly.
|
||||
type SimpleStreamAssemblerManager struct {
|
||||
InitLock sync.Mutex
|
||||
lastAssembler *SimpleStreamAssembler
|
||||
}
|
||||
|
||||
// New returns a new stream assembler.
|
||||
func (m *SimpleStreamAssemblerManager) New(net, transport gopacket.Flow) tcpassembly.Stream {
|
||||
assembler := new(SimpleStreamAssembler)
|
||||
m.lastAssembler = assembler
|
||||
return assembler
|
||||
}
|
||||
|
||||
// GetLastAssembler returns the newest created stream assembler.
|
||||
func (m *SimpleStreamAssemblerManager) GetLastAssembler() *SimpleStreamAssembler {
|
||||
return m.lastAssembler
|
||||
}
|
||||
|
||||
// SimpleStreamAssembler is a simple assembler for github.com/google/gopacket/tcpassembly.
|
||||
type SimpleStreamAssembler struct {
|
||||
Cumulated []byte
|
||||
CumulatedLen int
|
||||
Complete bool
|
||||
}
|
||||
|
||||
// NewSimpleStreamAssembler returns a new SimpleStreamAssembler.
|
||||
func NewSimpleStreamAssembler() *SimpleStreamAssembler {
|
||||
return &SimpleStreamAssembler{}
|
||||
}
|
||||
|
||||
// Reassembled implements tcpassembly.Stream's Reassembled function.
|
||||
func (a *SimpleStreamAssembler) Reassembled(reassembly []tcpassembly.Reassembly) {
|
||||
for _, entry := range reassembly {
|
||||
a.Cumulated = append(a.Cumulated, entry.Bytes...)
|
||||
}
|
||||
a.CumulatedLen = len(a.Cumulated)
|
||||
}
|
||||
|
||||
// ReassemblyComplete implements tcpassembly.Stream's ReassemblyComplete function.
|
||||
func (a *SimpleStreamAssembler) ReassemblyComplete() {
|
||||
a.Complete = true
|
||||
}
|
||||
Reference in New Issue
Block a user