diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 5d812e6b..c5d09f8a 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -138,10 +138,13 @@ Request: { "web":{"port":80,"ip":"192.168.11.33"}, "dns":{"port":53,"ip":"127.0.0.1","autofix":false}, + "set_static_ip": true | false } Server should check whether a port is available only in case it itself isn't already listening on that port. +If `set_static_ip` is `true`, Server attempts to set a static IP for the network interface chosen by `dns.ip` setting. If the operation is successful, `static_ip.static` setting will be `yes`. If it fails, `static_ip.static` setting will be set to `error` and `static_ip.error` will contain the error message. + Server replies on success: 200 OK @@ -149,7 +152,14 @@ Server replies on success: { "web":{"status":""}, "dns":{"status":""}, + "static_ip": { + "static": "yes|no|error", + "ip": "", // set if static=no + "error": "..." // set if static=error } + } + +If `static_ip.static` is `no`, Server has detected that the system uses a dynamic address and it can automatically set a static address if `set_static_ip` in request is `true`. See section `Static IP check/set` for detailed process. Server replies on error: @@ -172,7 +182,11 @@ Request: POST /control/install/check_config { - "dns":{"port":53,"ip":"127.0.0.1","autofix":false} + "dns":{ + "port":53, + "ip":"127.0.0.1", + "autofix":false + } } Check if DNSStubListener is enabled: @@ -499,13 +513,7 @@ which will print: default via 192.168.0.1 proto dhcp metric 100 -#### Phase 2 - -This method only works on Raspbian. - -On Ubuntu DHCP for a network interface can't be disabled via `dhcpcd.conf`. This must be configured in `/etc/netplan/01-netcfg.yaml`. - -Fedora doesn't use `dhcpcd.conf` configuration at all. +#### Phase 2 (Raspbian) Step 1. @@ -526,6 +534,44 @@ If we would set a different IP address, we'd need to replace the IP address for ip addr replace dev eth0 192.168.0.1/24 +#### Phase 2 (Ubuntu) + +`/etc/netplan/01-netcfg.yaml` or `/etc/netplan/01-network-manager-all.yaml` + +This configuration example has a static IP set for `enp0s3` interface: + + network: + version: 2 + renderer: networkd + ethernets: + enp0s3: + dhcp4: no + addresses: [192.168.0.2/24] + gateway: 192.168.0.1 + nameservers: + addresses: [192.168.0.1,8.8.8.8] + +For dynamic configuration `dhcp4: yes` is set. + +Make a backup copy to `/etc/netplan/01-netcfg.yaml.backup`. + +Apply: + + netplan apply + +Restart network: + + systemctl restart networking + +or: + + systemctl restart network-manager + +or: + + systemctl restart system-networkd + + ### Add a static lease Request: diff --git a/dhcpd/dhcp_http.go b/dhcpd/dhcp_http.go index e1b3d4fb..35f6a7ad 100644 --- a/dhcpd/dhcp_http.go +++ b/dhcpd/dhcp_http.go @@ -2,18 +2,14 @@ package dhcpd import ( "encoding/json" - "errors" "fmt" "io/ioutil" "net" "net/http" "os" - "os/exec" - "runtime" "strings" "time" - "github.com/AdguardTeam/golibs/file" "github.com/AdguardTeam/golibs/log" ) @@ -115,7 +111,7 @@ func (s *Server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) { } } -type netInterface struct { +type netInterfaceJSON struct { Name string `json:"name"` MTU int `json:"mtu"` HardwareAddr string `json:"hardware_address"` @@ -123,29 +119,6 @@ type netInterface struct { Flags string `json:"flags"` } -// getValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP -// invalid interface is a ppp interface or the one that doesn't allow broadcasts -func getValidNetInterfaces() ([]net.Interface, error) { - ifaces, err := net.Interfaces() - if err != nil { - return nil, fmt.Errorf("Couldn't get list of interfaces: %s", err) - } - - netIfaces := []net.Interface{} - - for i := range ifaces { - if ifaces[i].Flags&net.FlagPointToPoint != 0 { - // this interface is ppp, we're not interested in this one - continue - } - - iface := ifaces[i] - netIfaces = append(netIfaces, iface) - } - - return netIfaces, nil -} - func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{} @@ -170,7 +143,7 @@ func (s *Server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) { return } - jsonIface := netInterface{ + jsonIface := netInterfaceJSON{ Name: iface.Name, MTU: iface.MTU, HardwareAddr: iface.HardwareAddr.String(), @@ -263,137 +236,6 @@ func (s *Server) handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Reque } } -// Check if network interface has a static IP configured -func hasStaticIP(ifaceName string) (bool, error) { - if runtime.GOOS == "windows" { - return false, errors.New("Can't detect static IP: not supported on Windows") - } - - body, err := ioutil.ReadFile("/etc/dhcpcd.conf") - if err != nil { - return false, err - } - lines := strings.Split(string(body), "\n") - nameLine := fmt.Sprintf("interface %s", ifaceName) - withinInterfaceCtx := false - - for _, line := range lines { - line = strings.TrimSpace(line) - - if withinInterfaceCtx && len(line) == 0 { - // an empty line resets our state - withinInterfaceCtx = false - } - - if len(line) == 0 || line[0] == '#' { - continue - } - line = strings.TrimSpace(line) - - if !withinInterfaceCtx { - if line == nameLine { - // we found our interface - withinInterfaceCtx = true - } - - } else { - if strings.HasPrefix(line, "interface ") { - // we found another interface - reset our state - withinInterfaceCtx = false - continue - } - if strings.HasPrefix(line, "static ip_address=") { - return true, nil - } - } - } - - return false, nil -} - -// Get IP address with netmask -func getFullIP(ifaceName string) string { - cmd := exec.Command("ip", "-oneline", "-family", "inet", "address", "show", ifaceName) - log.Tracef("executing %s %v", cmd.Path, cmd.Args) - d, err := cmd.Output() - if err != nil || cmd.ProcessState.ExitCode() != 0 { - return "" - } - - fields := strings.Fields(string(d)) - if len(fields) < 4 { - return "" - } - _, _, err = net.ParseCIDR(fields[3]) - if err != nil { - return "" - } - - return fields[3] -} - -// Get gateway IP address -func getGatewayIP(ifaceName string) string { - cmd := exec.Command("ip", "route", "show", "dev", ifaceName) - log.Tracef("executing %s %v", cmd.Path, cmd.Args) - d, err := cmd.Output() - if err != nil || cmd.ProcessState.ExitCode() != 0 { - return "" - } - - fields := strings.Fields(string(d)) - if len(fields) < 3 || fields[0] != "default" { - return "" - } - - ip := net.ParseIP(fields[2]) - if ip == nil { - return "" - } - - return fields[2] -} - -// Set a static IP for network interface -func setStaticIP(ifaceName string) error { - ip := getFullIP(ifaceName) - if len(ip) == 0 { - return errors.New("Can't get IP address") - } - - body, err := ioutil.ReadFile("/etc/dhcpcd.conf") - if err != nil { - return err - } - - ip4, _, err := net.ParseCIDR(ip) - if err != nil { - return err - } - - add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n", - ifaceName, ip) - body = append(body, []byte(add)...) - - gatewayIP := getGatewayIP(ifaceName) - if len(gatewayIP) != 0 { - add = fmt.Sprintf("static routers=%s\n", - gatewayIP) - body = append(body, []byte(add)...) - } - - add = fmt.Sprintf("static domain_name_servers=%s\n\n", - ip4) - body = append(body, []byte(add)...) - - err = file.SafeWrite("/etc/dhcpcd.conf", body) - if err != nil { - return err - } - - return nil -} - func (s *Server) handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) { lj := staticLeaseJSON{} diff --git a/dhcpd/network_utils.go b/dhcpd/network_utils.go new file mode 100644 index 00000000..16a3c7dd --- /dev/null +++ b/dhcpd/network_utils.go @@ -0,0 +1,317 @@ +package dhcpd + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "github.com/AdguardTeam/golibs/file" + "github.com/AdguardTeam/golibs/log" + "github.com/joomcode/errorx" +) + +type netInterface struct { + Name string + MTU int + HardwareAddr string + Addresses []string + Flags string +} + +// getValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP +// invalid interface is a ppp interface or the one that doesn't allow broadcasts +func getValidNetInterfaces() ([]net.Interface, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("Couldn't get list of interfaces: %s", err) + } + + netIfaces := []net.Interface{} + + for i := range ifaces { + if ifaces[i].Flags&net.FlagPointToPoint != 0 { + // this interface is ppp, we're not interested in this one + continue + } + + iface := ifaces[i] + netIfaces = append(netIfaces, iface) + } + + return netIfaces, nil +} + +// getValidNetInterfacesMap returns interfaces that are eligible for DNS and WEB only +// we do not return link-local addresses here +func getValidNetInterfacesForWeb() ([]netInterface, error) { + ifaces, err := getValidNetInterfaces() + if err != nil { + return nil, errorx.Decorate(err, "Couldn't get interfaces") + } + if len(ifaces) == 0 { + return nil, errors.New("couldn't find any legible interface") + } + + var netInterfaces []netInterface + + for _, iface := range ifaces { + addrs, e := iface.Addrs() + if e != nil { + return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name) + } + + netIface := netInterface{ + Name: iface.Name, + MTU: iface.MTU, + HardwareAddr: iface.HardwareAddr.String(), + } + + if iface.Flags != 0 { + netIface.Flags = iface.Flags.String() + } + + // we don't want link-local addresses in json, so skip them + for _, addr := range addrs { + ipnet, ok := addr.(*net.IPNet) + if !ok { + // not an IPNet, should not happen + return nil, fmt.Errorf("got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr) + } + // ignore link-local + if ipnet.IP.IsLinkLocalUnicast() { + continue + } + netIface.Addresses = append(netIface.Addresses, ipnet.IP.String()) + } + if len(netIface.Addresses) != 0 { + netInterfaces = append(netInterfaces, netIface) + } + } + + return netInterfaces, nil +} + +// Check if network interface has a static IP configured +// Supports: Raspbian. +func hasStaticIP(ifaceName string) (bool, error) { + if runtime.GOOS == "windows" { + return false, errors.New("Can't detect static IP: not supported on Windows") + } + + body, err := ioutil.ReadFile("/etc/dhcpcd.conf") + if err != nil { + return false, err + } + + return hasStaticIPDhcpcdConf(string(body), ifaceName), nil +} + +// for dhcpcd.conf +func hasStaticIPDhcpcdConf(data, ifaceName string) bool { + lines := strings.Split(data, "\n") + nameLine := fmt.Sprintf("interface %s", ifaceName) + withinInterfaceCtx := false + + for _, line := range lines { + line = strings.TrimSpace(line) + + if withinInterfaceCtx && len(line) == 0 { + // an empty line resets our state + withinInterfaceCtx = false + } + + if len(line) == 0 || line[0] == '#' { + continue + } + line = strings.TrimSpace(line) + + if !withinInterfaceCtx { + if line == nameLine { + // we found our interface + withinInterfaceCtx = true + } + + } else { + if strings.HasPrefix(line, "interface ") { + // we found another interface - reset our state + withinInterfaceCtx = false + continue + } + if strings.HasPrefix(line, "static ip_address=") { + return true + } + } + } + return false +} + +// Get IP address with netmask +func getFullIP(ifaceName string) string { + cmd := exec.Command("ip", "-oneline", "-family", "inet", "address", "show", ifaceName) + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + d, err := cmd.Output() + if err != nil || cmd.ProcessState.ExitCode() != 0 { + return "" + } + + fields := strings.Fields(string(d)) + if len(fields) < 4 { + return "" + } + _, _, err = net.ParseCIDR(fields[3]) + if err != nil { + return "" + } + + return fields[3] +} + +// Get interface name by its IP address. +func getInterfaceByIP(ip string) string { + ifaces, err := getValidNetInterfacesForWeb() + if err != nil { + return "" + } + + for _, iface := range ifaces { + for _, addr := range iface.Addresses { + if ip == addr { + return iface.Name + } + } + } + + return "" +} + +// Get gateway IP address +func getGatewayIP(ifaceName string) string { + cmd := exec.Command("ip", "route", "show", "dev", ifaceName) + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + d, err := cmd.Output() + if err != nil || cmd.ProcessState.ExitCode() != 0 { + return "" + } + + fields := strings.Fields(string(d)) + if len(fields) < 3 || fields[0] != "default" { + return "" + } + + ip := net.ParseIP(fields[2]) + if ip == nil { + return "" + } + + return fields[2] +} + +// Set a static IP for network interface +// Supports: Raspbian. +func setStaticIP(ifaceName string) error { + ip := getFullIP(ifaceName) + if len(ip) == 0 { + return errors.New("Can't get IP address") + } + + ip4, _, err := net.ParseCIDR(ip) + if err != nil { + return err + } + gatewayIP := getGatewayIP(ifaceName) + add := setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, ip4.String()) + + body, err := ioutil.ReadFile("/etc/dhcpcd.conf") + if err != nil { + return err + } + + body = append(body, []byte(add)...) + err = file.SafeWrite("/etc/dhcpcd.conf", body) + if err != nil { + return err + } + + return nil +} + +// for dhcpcd.conf +func setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string { + var body []byte + + add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n", + ifaceName, ip) + body = append(body, []byte(add)...) + + if len(gatewayIP) != 0 { + add = fmt.Sprintf("static routers=%s\n", + gatewayIP) + body = append(body, []byte(add)...) + } + + add = fmt.Sprintf("static domain_name_servers=%s\n\n", + dnsIP) + body = append(body, []byte(add)...) + + return string(body) +} + +// checkPortAvailable is not a cheap test to see if the port is bindable, because it's actually doing the bind momentarily +func checkPortAvailable(host string, port int) error { + ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))) + if err != nil { + return err + } + ln.Close() + + // It seems that net.Listener.Close() doesn't close file descriptors right away. + // We wait for some time and hope that this fd will be closed. + time.Sleep(100 * time.Millisecond) + return nil +} + +func checkPacketPortAvailable(host string, port int) error { + ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port))) + if err != nil { + return err + } + ln.Close() + + // It seems that net.Listener.Close() doesn't close file descriptors right away. + // We wait for some time and hope that this fd will be closed. + time.Sleep(100 * time.Millisecond) + return err +} + +// check if error is "address already in use" +func errorIsAddrInUse(err error) bool { + errOpError, ok := err.(*net.OpError) + if !ok { + return false + } + + errSyscallError, ok := errOpError.Err.(*os.SyscallError) + if !ok { + return false + } + + errErrno, ok := errSyscallError.Err.(syscall.Errno) + if !ok { + return false + } + + if runtime.GOOS == "windows" { + const WSAEADDRINUSE = 10048 + return errErrno == WSAEADDRINUSE + } + + return errErrno == syscall.EADDRINUSE +} diff --git a/home/control_install.go b/home/control_install.go index 5311c091..18fe2a8b 100644 --- a/home/control_install.go +++ b/home/control_install.go @@ -22,6 +22,14 @@ type firstRunData struct { Interfaces map[string]interface{} `json:"interfaces"` } +type netInterfaceJSON struct { + Name string `json:"name"` + MTU int `json:"mtu"` + HardwareAddr string `json:"hardware_address"` + Addresses []string `json:"ip_addresses"` + Flags string `json:"flags"` +} + // Get initial installation settings func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { data := firstRunData{} @@ -36,7 +44,14 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { data.Interfaces = make(map[string]interface{}) for _, iface := range ifaces { - data.Interfaces[iface.Name] = iface + ifaceJSON := netInterfaceJSON{ + Name: iface.Name, + MTU: iface.MTU, + HardwareAddr: iface.HardwareAddr, + Addresses: iface.Addresses, + Flags: iface.Flags, + } + data.Interfaces[iface.Name] = ifaceJSON } w.Header().Set("Content-Type", "application/json") @@ -48,9 +63,10 @@ func handleInstallGetAddresses(w http.ResponseWriter, r *http.Request) { } type checkConfigReqEnt struct { - Port int `json:"port"` - IP string `json:"ip"` - Autofix bool `json:"autofix"` + Port int `json:"port"` + IP string `json:"ip"` + Autofix bool `json:"autofix"` + SetStaticIP bool `json:"set_static_ip"` } type checkConfigReq struct { Web checkConfigReqEnt `json:"web"` @@ -61,9 +77,15 @@ type checkConfigRespEnt struct { Status string `json:"status"` CanAutofix bool `json:"can_autofix"` } +type staticIPJSON struct { + Static string `json:"static"` + IP string `json:"ip"` + Error string `json:"error"` +} type checkConfigResp struct { - Web checkConfigRespEnt `json:"web"` - DNS checkConfigRespEnt `json:"dns"` + Web checkConfigRespEnt `json:"web"` + DNS checkConfigRespEnt `json:"dns"` + StaticIP staticIPJSON `json:"static_ip"` } // Check if ports are available, respond with results @@ -108,6 +130,33 @@ func handleInstallCheckConfig(w http.ResponseWriter, r *http.Request) { if err != nil { respData.DNS.Status = fmt.Sprintf("%v", err) + + } else { + + interfaceName := getInterfaceByIP(reqData.DNS.IP) + staticIPStatus := "yes" + + if len(interfaceName) == 0 { + staticIPStatus = "error" + respData.StaticIP.Error = fmt.Sprintf("Couldn't find network interface by IP %s", reqData.DNS.IP) + + } else if reqData.DNS.SetStaticIP { + err = setStaticIP(interfaceName) + staticIPStatus = "error" + respData.StaticIP.Error = err.Error() + + } else { + // check if we have a static IP + isStaticIP, err := hasStaticIP(interfaceName) + if err != nil { + staticIPStatus = "error" + respData.StaticIP.Error = err.Error() + } else if !isStaticIP { + staticIPStatus = "no" + respData.StaticIP.IP = getFullIP(interfaceName) + } + } + respData.StaticIP.Static = staticIPStatus } } diff --git a/home/helpers.go b/home/helpers.go index c00fcbc8..e05c4bb2 100644 --- a/home/helpers.go +++ b/home/helpers.go @@ -2,7 +2,6 @@ package home import ( "context" - "errors" "fmt" "net" "net/http" @@ -13,7 +12,6 @@ import ( "runtime" "strconv" "strings" - "syscall" "time" "github.com/AdguardTeam/golibs/log" @@ -151,117 +149,6 @@ func postInstallHandler(handler http.Handler) http.Handler { return &postInstallHandlerStruct{handler} } -// ------------------ -// network interfaces -// ------------------ -type netInterface struct { - Name string `json:"name"` - MTU int `json:"mtu"` - HardwareAddr string `json:"hardware_address"` - Addresses []string `json:"ip_addresses"` - Flags string `json:"flags"` -} - -// getValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP -// invalid interface is a ppp interface or the one that doesn't allow broadcasts -func getValidNetInterfaces() ([]net.Interface, error) { - ifaces, err := net.Interfaces() - if err != nil { - return nil, fmt.Errorf("Couldn't get list of interfaces: %s", err) - } - - netIfaces := []net.Interface{} - - for i := range ifaces { - if ifaces[i].Flags&net.FlagPointToPoint != 0 { - // this interface is ppp, we're not interested in this one - continue - } - - iface := ifaces[i] - netIfaces = append(netIfaces, iface) - } - - return netIfaces, nil -} - -// getValidNetInterfacesMap returns interfaces that are eligible for DNS and WEB only -// we do not return link-local addresses here -func getValidNetInterfacesForWeb() ([]netInterface, error) { - ifaces, err := getValidNetInterfaces() - if err != nil { - return nil, errorx.Decorate(err, "Couldn't get interfaces") - } - if len(ifaces) == 0 { - return nil, errors.New("couldn't find any legible interface") - } - - var netInterfaces []netInterface - - for _, iface := range ifaces { - addrs, e := iface.Addrs() - if e != nil { - return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name) - } - - netIface := netInterface{ - Name: iface.Name, - MTU: iface.MTU, - HardwareAddr: iface.HardwareAddr.String(), - } - - if iface.Flags != 0 { - netIface.Flags = iface.Flags.String() - } - - // we don't want link-local addresses in json, so skip them - for _, addr := range addrs { - ipnet, ok := addr.(*net.IPNet) - if !ok { - // not an IPNet, should not happen - return nil, fmt.Errorf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr) - } - // ignore link-local - if ipnet.IP.IsLinkLocalUnicast() { - continue - } - netIface.Addresses = append(netIface.Addresses, ipnet.IP.String()) - } - if len(netIface.Addresses) != 0 { - netInterfaces = append(netInterfaces, netIface) - } - } - - return netInterfaces, nil -} - -// checkPortAvailable is not a cheap test to see if the port is bindable, because it's actually doing the bind momentarily -func checkPortAvailable(host string, port int) error { - ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))) - if err != nil { - return err - } - ln.Close() - - // It seems that net.Listener.Close() doesn't close file descriptors right away. - // We wait for some time and hope that this fd will be closed. - time.Sleep(100 * time.Millisecond) - return nil -} - -func checkPacketPortAvailable(host string, port int) error { - ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port))) - if err != nil { - return err - } - ln.Close() - - // It seems that net.Listener.Close() doesn't close file descriptors right away. - // We wait for some time and hope that this fd will be closed. - time.Sleep(100 * time.Millisecond) - return err -} - // Connect to a remote server resolving hostname using our own DNS server func customDialContext(ctx context.Context, network, addr string) (net.Conn, error) { log.Tracef("network:%v addr:%v", network, addr) @@ -303,31 +190,6 @@ func customDialContext(ctx context.Context, network, addr string) (net.Conn, err return nil, errorx.DecorateMany(fmt.Sprintf("couldn't dial to %s", addr), dialErrs...) } -// check if error is "address already in use" -func errorIsAddrInUse(err error) bool { - errOpError, ok := err.(*net.OpError) - if !ok { - return false - } - - errSyscallError, ok := errOpError.Err.(*os.SyscallError) - if !ok { - return false - } - - errErrno, ok := errSyscallError.Err.(syscall.Errno) - if !ok { - return false - } - - if runtime.GOOS == "windows" { - const WSAEADDRINUSE = 10048 - return errErrno == WSAEADDRINUSE - } - - return errErrno == syscall.EADDRINUSE -} - // --------------------- // debug logging helpers // --------------------- diff --git a/home/network_utils.go b/home/network_utils.go new file mode 100644 index 00000000..04112001 --- /dev/null +++ b/home/network_utils.go @@ -0,0 +1,317 @@ +package home + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "github.com/AdguardTeam/golibs/file" + "github.com/AdguardTeam/golibs/log" + "github.com/joomcode/errorx" +) + +type netInterface struct { + Name string + MTU int + HardwareAddr string + Addresses []string + Flags string +} + +// getValidNetInterfaces returns interfaces that are eligible for DNS and/or DHCP +// invalid interface is a ppp interface or the one that doesn't allow broadcasts +func getValidNetInterfaces() ([]net.Interface, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("Couldn't get list of interfaces: %s", err) + } + + netIfaces := []net.Interface{} + + for i := range ifaces { + if ifaces[i].Flags&net.FlagPointToPoint != 0 { + // this interface is ppp, we're not interested in this one + continue + } + + iface := ifaces[i] + netIfaces = append(netIfaces, iface) + } + + return netIfaces, nil +} + +// getValidNetInterfacesMap returns interfaces that are eligible for DNS and WEB only +// we do not return link-local addresses here +func getValidNetInterfacesForWeb() ([]netInterface, error) { + ifaces, err := getValidNetInterfaces() + if err != nil { + return nil, errorx.Decorate(err, "Couldn't get interfaces") + } + if len(ifaces) == 0 { + return nil, errors.New("couldn't find any legible interface") + } + + var netInterfaces []netInterface + + for _, iface := range ifaces { + addrs, e := iface.Addrs() + if e != nil { + return nil, errorx.Decorate(e, "Failed to get addresses for interface %s", iface.Name) + } + + netIface := netInterface{ + Name: iface.Name, + MTU: iface.MTU, + HardwareAddr: iface.HardwareAddr.String(), + } + + if iface.Flags != 0 { + netIface.Flags = iface.Flags.String() + } + + // we don't want link-local addresses in json, so skip them + for _, addr := range addrs { + ipnet, ok := addr.(*net.IPNet) + if !ok { + // not an IPNet, should not happen + return nil, fmt.Errorf("got iface.Addrs() element %s that is not net.IPNet, it is %T", addr, addr) + } + // ignore link-local + if ipnet.IP.IsLinkLocalUnicast() { + continue + } + netIface.Addresses = append(netIface.Addresses, ipnet.IP.String()) + } + if len(netIface.Addresses) != 0 { + netInterfaces = append(netInterfaces, netIface) + } + } + + return netInterfaces, nil +} + +// Check if network interface has a static IP configured +// Supports: Raspbian. +func hasStaticIP(ifaceName string) (bool, error) { + if runtime.GOOS == "windows" { + return false, errors.New("Can't detect static IP: not supported on Windows") + } + + body, err := ioutil.ReadFile("/etc/dhcpcd.conf") + if err != nil { + return false, err + } + + return hasStaticIPDhcpcdConf(string(body), ifaceName), nil +} + +// for dhcpcd.conf +func hasStaticIPDhcpcdConf(data, ifaceName string) bool { + lines := strings.Split(data, "\n") + nameLine := fmt.Sprintf("interface %s", ifaceName) + withinInterfaceCtx := false + + for _, line := range lines { + line = strings.TrimSpace(line) + + if withinInterfaceCtx && len(line) == 0 { + // an empty line resets our state + withinInterfaceCtx = false + } + + if len(line) == 0 || line[0] == '#' { + continue + } + line = strings.TrimSpace(line) + + if !withinInterfaceCtx { + if line == nameLine { + // we found our interface + withinInterfaceCtx = true + } + + } else { + if strings.HasPrefix(line, "interface ") { + // we found another interface - reset our state + withinInterfaceCtx = false + continue + } + if strings.HasPrefix(line, "static ip_address=") { + return true + } + } + } + return false +} + +// Get IP address with netmask +func getFullIP(ifaceName string) string { + cmd := exec.Command("ip", "-oneline", "-family", "inet", "address", "show", ifaceName) + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + d, err := cmd.Output() + if err != nil || cmd.ProcessState.ExitCode() != 0 { + return "" + } + + fields := strings.Fields(string(d)) + if len(fields) < 4 { + return "" + } + _, _, err = net.ParseCIDR(fields[3]) + if err != nil { + return "" + } + + return fields[3] +} + +// Get interface name by its IP address. +func getInterfaceByIP(ip string) string { + ifaces, err := getValidNetInterfacesForWeb() + if err != nil { + return "" + } + + for _, iface := range ifaces { + for _, addr := range iface.Addresses { + if ip == addr { + return iface.Name + } + } + } + + return "" +} + +// Get gateway IP address +func getGatewayIP(ifaceName string) string { + cmd := exec.Command("ip", "route", "show", "dev", ifaceName) + log.Tracef("executing %s %v", cmd.Path, cmd.Args) + d, err := cmd.Output() + if err != nil || cmd.ProcessState.ExitCode() != 0 { + return "" + } + + fields := strings.Fields(string(d)) + if len(fields) < 3 || fields[0] != "default" { + return "" + } + + ip := net.ParseIP(fields[2]) + if ip == nil { + return "" + } + + return fields[2] +} + +// Set a static IP for network interface +// Supports: Raspbian. +func setStaticIP(ifaceName string) error { + ip := getFullIP(ifaceName) + if len(ip) == 0 { + return errors.New("Can't get IP address") + } + + ip4, _, err := net.ParseCIDR(ip) + if err != nil { + return err + } + gatewayIP := getGatewayIP(ifaceName) + add := setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, ip4.String()) + + body, err := ioutil.ReadFile("/etc/dhcpcd.conf") + if err != nil { + return err + } + + body = append(body, []byte(add)...) + err = file.SafeWrite("/etc/dhcpcd.conf", body) + if err != nil { + return err + } + + return nil +} + +// for dhcpcd.conf +func setStaticIPDhcpcdConf(ifaceName, ip, gatewayIP, dnsIP string) string { + var body []byte + + add := fmt.Sprintf("\ninterface %s\nstatic ip_address=%s\n", + ifaceName, ip) + body = append(body, []byte(add)...) + + if len(gatewayIP) != 0 { + add = fmt.Sprintf("static routers=%s\n", + gatewayIP) + body = append(body, []byte(add)...) + } + + add = fmt.Sprintf("static domain_name_servers=%s\n\n", + dnsIP) + body = append(body, []byte(add)...) + + return string(body) +} + +// checkPortAvailable is not a cheap test to see if the port is bindable, because it's actually doing the bind momentarily +func checkPortAvailable(host string, port int) error { + ln, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))) + if err != nil { + return err + } + ln.Close() + + // It seems that net.Listener.Close() doesn't close file descriptors right away. + // We wait for some time and hope that this fd will be closed. + time.Sleep(100 * time.Millisecond) + return nil +} + +func checkPacketPortAvailable(host string, port int) error { + ln, err := net.ListenPacket("udp", net.JoinHostPort(host, strconv.Itoa(port))) + if err != nil { + return err + } + ln.Close() + + // It seems that net.Listener.Close() doesn't close file descriptors right away. + // We wait for some time and hope that this fd will be closed. + time.Sleep(100 * time.Millisecond) + return err +} + +// check if error is "address already in use" +func errorIsAddrInUse(err error) bool { + errOpError, ok := err.(*net.OpError) + if !ok { + return false + } + + errSyscallError, ok := errOpError.Err.(*os.SyscallError) + if !ok { + return false + } + + errErrno, ok := errSyscallError.Err.(syscall.Errno) + if !ok { + return false + } + + if runtime.GOOS == "windows" { + const WSAEADDRINUSE = 10048 + return errErrno == WSAEADDRINUSE + } + + return errErrno == syscall.EADDRINUSE +} diff --git a/home/network_utils_test.go b/home/network_utils_test.go new file mode 100644 index 00000000..6312e535 --- /dev/null +++ b/home/network_utils_test.go @@ -0,0 +1,61 @@ +package home + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHasStaticIPDhcpcdConf(t *testing.T) { + dhcpdConf := `#comment +# comment + +interface eth0 +static ip_address=192.168.0.1/24 + +# interface wlan0 +static ip_address=192.168.1.1/24 + +# comment +` + assert.True(t, !hasStaticIPDhcpcdConf(dhcpdConf, "wlan0")) + + dhcpdConf = `#comment +# comment + +interface eth0 +static ip_address=192.168.0.1/24 + +# interface wlan0 +static ip_address=192.168.1.1/24 + +# comment + +interface wlan0 +# comment +static ip_address=192.168.2.1/24 +` + assert.True(t, hasStaticIPDhcpcdConf(dhcpdConf, "wlan0")) +} + +func TestSetStaticIPDhcpcdConf(t *testing.T) { + dhcpcdConf := ` +interface wlan0 +static ip_address=192.168.0.2/24 +static routers=192.168.0.1 +static domain_name_servers=192.168.0.2 + +` + s := setStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "192.168.0.1", "192.168.0.2") + assert.Equal(t, dhcpcdConf, s) + + // without gateway + dhcpcdConf = ` +interface wlan0 +static ip_address=192.168.0.2/24 +static domain_name_servers=192.168.0.2 + +` + s = setStaticIPDhcpcdConf("wlan0", "192.168.0.2/24", "", "192.168.0.2") + assert.Equal(t, dhcpcdConf, s) +}