2023-06-12 14:43:55 +02:00
//go:build !android
2022-11-23 13:39:42 +01:00
package dns
import (
"bytes"
"fmt"
2024-01-30 09:58:56 +01:00
"net/netip"
2022-11-23 13:39:42 +01:00
"os"
2023-11-03 13:05:39 +01:00
"strings"
2024-03-01 15:17:35 +01:00
"time"
2023-02-13 15:25:11 +01:00
log "github.com/sirupsen/logrus"
2022-11-23 13:39:42 +01:00
)
const (
2023-11-03 13:05:39 +01:00
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
fileGeneratedResolvConfContentHeaderNextLine = fileGeneratedResolvConfContentHeader + `
# If needed you can restore the original file by copying back ` + fileDefaultResolvConfBackupLocation + "\n\n"
2023-02-13 15:25:11 +01:00
2022-11-23 13:39:42 +01:00
fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird"
2023-11-03 13:05:39 +01:00
fileMaxLineCharsLimit = 256
fileMaxNumberOfSearchDomains = 6
)
2022-11-23 13:39:42 +01:00
2024-03-01 15:17:35 +01:00
const (
dnsFailoverTimeout = 4 * time . Second
dnsFailoverAttempts = 1
)
2022-11-23 13:39:42 +01:00
type fileConfigurator struct {
2024-01-24 16:47:26 +01:00
repair * repair
2024-03-01 15:17:35 +01:00
originalPerms os . FileMode
nbNameserverIP string
2022-11-23 13:39:42 +01:00
}
func newFileConfigurator ( ) ( hostManager , error ) {
2024-01-24 16:47:26 +01:00
fc := & fileConfigurator { }
fc . repair = newRepair ( defaultResolvConfPath , fc . updateConfig )
return fc , nil
2022-11-23 13:39:42 +01:00
}
2023-05-17 00:03:26 +02:00
func ( f * fileConfigurator ) supportCustomPort ( ) bool {
return false
}
2023-12-18 11:46:58 +01:00
func ( f * fileConfigurator ) applyDNSConfig ( config HostDNSConfig ) error {
2024-05-06 17:01:00 +02:00
backupFileExist := f . isBackupFileExist ( )
2023-12-18 11:46:58 +01:00
if ! config . RouteAll {
2022-11-23 13:39:42 +01:00
if backupFileExist {
2024-05-06 17:01:00 +02:00
f . repair . stopWatchFileChanges ( )
err := f . restore ( )
2022-11-23 13:39:42 +01:00
if err != nil {
2024-05-06 17:01:00 +02:00
return fmt . Errorf ( "restoring the original resolv.conf file return err: %w" , err )
2022-11-23 13:39:42 +01:00
}
}
2022-11-29 14:51:18 +01:00
return fmt . Errorf ( "unable to configure DNS for this peer using file manager without a nameserver group with all domains configured" )
2022-11-23 13:39:42 +01:00
}
2023-11-03 13:05:39 +01:00
if ! backupFileExist {
2024-05-06 17:01:00 +02:00
err := f . backup ( )
2023-11-03 13:05:39 +01:00
if err != nil {
2024-01-30 09:58:56 +01:00
return fmt . Errorf ( "unable to backup the resolv.conf file: %w" , err )
2022-11-23 13:39:42 +01:00
}
}
2023-08-01 17:45:44 +02:00
2024-01-24 16:47:26 +01:00
nbSearchDomains := searchDomains ( config )
2024-03-01 15:17:35 +01:00
f . nbNameserverIP = config . ServerIP
2023-11-03 13:05:39 +01:00
2024-01-24 16:47:26 +01:00
resolvConf , err := parseBackupResolvConf ( )
2023-08-01 17:45:44 +02:00
if err != nil {
2024-01-30 09:58:56 +01:00
log . Errorf ( "could not read original search domains from %s: %s" , fileDefaultResolvConfBackupLocation , err )
2023-08-01 17:45:44 +02:00
}
2023-11-03 13:05:39 +01:00
2024-01-24 16:47:26 +01:00
f . repair . stopWatchFileChanges ( )
2024-03-01 15:17:35 +01:00
err = f . updateConfig ( nbSearchDomains , f . nbNameserverIP , resolvConf )
2024-01-24 16:47:26 +01:00
if err != nil {
return err
}
2024-03-01 15:17:35 +01:00
f . repair . watchFileChanges ( nbSearchDomains , f . nbNameserverIP )
2024-01-24 16:47:26 +01:00
return nil
}
func ( f * fileConfigurator ) updateConfig ( nbSearchDomains [ ] string , nbNameserverIP string , cfg * resolvConf ) error {
searchDomainList := mergeSearchDomains ( nbSearchDomains , cfg . searchDomains )
nameServers := generateNsList ( nbNameserverIP , cfg )
2023-11-03 13:05:39 +01:00
2024-03-01 15:17:35 +01:00
options := prepareOptionsWithTimeout ( cfg . others , int ( dnsFailoverTimeout . Seconds ( ) ) , dnsFailoverAttempts )
2023-11-03 13:05:39 +01:00
buf := prepareResolvConfContent (
searchDomainList ,
2024-01-24 16:47:26 +01:00
nameServers ,
2024-03-01 15:17:35 +01:00
options )
2023-11-03 13:05:39 +01:00
log . Debugf ( "creating managed file %s" , defaultResolvConfPath )
2024-01-24 16:47:26 +01:00
err := os . WriteFile ( defaultResolvConfPath , buf . Bytes ( ) , f . originalPerms )
2022-11-23 13:39:42 +01:00
if err != nil {
2023-11-03 13:05:39 +01:00
restoreErr := f . restore ( )
if restoreErr != nil {
2022-11-23 13:39:42 +01:00
log . Errorf ( "attempt to restore default file failed with error: %s" , err )
}
2024-01-30 09:58:56 +01:00
return fmt . Errorf ( "creating resolver file %s. Error: %w" , defaultResolvConfPath , err )
}
log . Infof ( "created a NetBird managed %s file with the DNS settings. Added %d search domains. Search list: %s" , defaultResolvConfPath , len ( searchDomainList ) , searchDomainList )
// create another backup for unclean shutdown detection right after overwriting the original resolv.conf
if err := createUncleanShutdownIndicator ( fileDefaultResolvConfBackupLocation , fileManager , nbNameserverIP ) ; err != nil {
log . Errorf ( "failed to create unclean shutdown resolv.conf backup: %s" , err )
2022-11-23 13:39:42 +01:00
}
2023-11-03 13:05:39 +01:00
2022-11-23 13:39:42 +01:00
return nil
}
func ( f * fileConfigurator ) restoreHostDNS ( ) error {
2024-01-24 16:47:26 +01:00
f . repair . stopWatchFileChanges ( )
2022-11-23 13:39:42 +01:00
return f . restore ( )
}
func ( f * fileConfigurator ) backup ( ) error {
stats , err := os . Stat ( defaultResolvConfPath )
if err != nil {
2024-01-30 09:58:56 +01:00
return fmt . Errorf ( "checking stats for %s file. Error: %w" , defaultResolvConfPath , err )
2022-11-23 13:39:42 +01:00
}
f . originalPerms = stats . Mode ( )
err = copyFile ( defaultResolvConfPath , fileDefaultResolvConfBackupLocation )
if err != nil {
2024-01-30 09:58:56 +01:00
return fmt . Errorf ( "backing up %s: %w" , defaultResolvConfPath , err )
2022-11-23 13:39:42 +01:00
}
return nil
}
func ( f * fileConfigurator ) restore ( ) error {
2024-03-01 15:17:35 +01:00
err := removeFirstNbNameserver ( fileDefaultResolvConfBackupLocation , f . nbNameserverIP )
if err != nil {
log . Errorf ( "Failed to remove netbird nameserver from %s on backup restore: %s" , fileDefaultResolvConfBackupLocation , err )
}
err = copyFile ( fileDefaultResolvConfBackupLocation , defaultResolvConfPath )
2022-11-23 13:39:42 +01:00
if err != nil {
2024-01-30 09:58:56 +01:00
return fmt . Errorf ( "restoring %s from %s: %w" , defaultResolvConfPath , fileDefaultResolvConfBackupLocation , err )
}
if err := removeUncleanShutdownIndicator ( ) ; err != nil {
log . Errorf ( "failed to remove unclean shutdown resolv.conf backup: %s" , err )
2022-11-23 13:39:42 +01:00
}
return os . RemoveAll ( fileDefaultResolvConfBackupLocation )
}
2024-01-30 09:58:56 +01:00
func ( f * fileConfigurator ) restoreUncleanShutdownDNS ( storedDNSAddress * netip . Addr ) error {
resolvConf , err := parseDefaultResolvConf ( )
if err != nil {
return fmt . Errorf ( "parse current resolv.conf: %w" , err )
}
// no current nameservers set -> restore
if len ( resolvConf . nameServers ) == 0 {
return restoreResolvConfFile ( )
}
currentDNSAddress , err := netip . ParseAddr ( resolvConf . nameServers [ 0 ] )
// not a valid first nameserver -> restore
if err != nil {
2024-03-01 15:17:35 +01:00
log . Errorf ( "restoring unclean shutdown: parse dns address %s failed: %s" , resolvConf . nameServers [ 0 ] , err )
2024-01-30 09:58:56 +01:00
return restoreResolvConfFile ( )
}
// current address is still netbird's non-available dns address -> restore
// comparing parsed addresses only, to remove ambiguity
if currentDNSAddress . String ( ) == storedDNSAddress . String ( ) {
return restoreResolvConfFile ( )
}
log . Info ( "restoring unclean shutdown: first current nameserver differs from saved nameserver pre-netbird: not restoring" )
return nil
}
2024-05-06 17:01:00 +02:00
func ( f * fileConfigurator ) isBackupFileExist ( ) bool {
_ , err := os . Stat ( fileDefaultResolvConfBackupLocation )
return err == nil
}
2024-01-30 09:58:56 +01:00
func restoreResolvConfFile ( ) error {
log . Debugf ( "restoring unclean shutdown: restoring %s from %s" , defaultResolvConfPath , fileUncleanShutdownResolvConfLocation )
if err := copyFile ( fileUncleanShutdownResolvConfLocation , defaultResolvConfPath ) ; err != nil {
return fmt . Errorf ( "restoring %s from %s: %w" , defaultResolvConfPath , fileUncleanShutdownResolvConfLocation , err )
}
if err := removeUncleanShutdownIndicator ( ) ; err != nil {
log . Errorf ( "failed to remove unclean shutdown resolv.conf file: %s" , err )
}
return nil
}
2024-01-24 16:47:26 +01:00
// generateNsList generates a list of nameservers from the config and adds the primary nameserver to the beginning of the list
func generateNsList ( nbNameserverIP string , cfg * resolvConf ) [ ] string {
ns := make ( [ ] string , 1 , len ( cfg . nameServers ) + 1 )
ns [ 0 ] = nbNameserverIP
for _ , cfgNs := range cfg . nameServers {
if nbNameserverIP != cfgNs {
ns = append ( ns , cfgNs )
}
}
return ns
}
2023-11-03 13:05:39 +01:00
func prepareResolvConfContent ( searchDomains , nameServers , others [ ] string ) bytes . Buffer {
2022-11-23 13:39:42 +01:00
var buf bytes . Buffer
2023-11-03 13:05:39 +01:00
buf . WriteString ( fileGeneratedResolvConfContentHeaderNextLine )
for _ , cfgLine := range others {
buf . WriteString ( cfgLine )
buf . WriteString ( "\n" )
}
if len ( searchDomains ) > 0 {
buf . WriteString ( "search " )
buf . WriteString ( strings . Join ( searchDomains , " " ) )
buf . WriteString ( "\n" )
}
for _ , ns := range nameServers {
buf . WriteString ( "nameserver " )
buf . WriteString ( ns )
buf . WriteString ( "\n" )
}
return buf
}
2023-12-18 11:46:58 +01:00
func searchDomains ( config HostDNSConfig ) [ ] string {
2023-11-03 13:05:39 +01:00
listOfDomains := make ( [ ] string , 0 )
2023-12-18 11:46:58 +01:00
for _ , dConf := range config . Domains {
if dConf . MatchOnly || dConf . Disabled {
2023-11-03 13:05:39 +01:00
continue
}
2023-12-18 11:46:58 +01:00
listOfDomains = append ( listOfDomains , dConf . Domain )
2023-11-03 13:05:39 +01:00
}
return listOfDomains
}
2023-12-18 11:46:58 +01:00
// merge search Domains lists and cut off the list if it is too long
2023-11-03 13:05:39 +01:00
func mergeSearchDomains ( searchDomains [ ] string , originalSearchDomains [ ] string ) [ ] string {
lineSize := len ( "search" )
searchDomainsList := make ( [ ] string , 0 , len ( searchDomains ) + len ( originalSearchDomains ) )
lineSize = validateAndFillSearchDomains ( lineSize , & searchDomainsList , searchDomains )
_ = validateAndFillSearchDomains ( lineSize , & searchDomainsList , originalSearchDomains )
return searchDomainsList
}
2023-12-18 11:46:58 +01:00
// validateAndFillSearchDomains checks if the search Domains list is not too long and if the line is not too long
2023-11-03 13:05:39 +01:00
// extend s slice with vs elements
// return with the number of characters in the searchDomains line
func validateAndFillSearchDomains ( initialLineChars int , s * [ ] string , vs [ ] string ) int {
for _ , sd := range vs {
2024-01-24 16:47:26 +01:00
duplicated := false
for _ , fs := range * s {
if fs == sd {
duplicated = true
break
}
}
if duplicated {
continue
}
2023-11-03 13:05:39 +01:00
tmpCharsNumber := initialLineChars + 1 + len ( sd )
if tmpCharsNumber > fileMaxLineCharsLimit {
2023-12-18 11:46:58 +01:00
// lets log all skipped Domains
2023-11-03 13:05:39 +01:00
log . Infof ( "search list line is larger than %d characters. Skipping append of %s domain" , fileMaxLineCharsLimit , sd )
continue
}
initialLineChars = tmpCharsNumber
if len ( * s ) >= fileMaxNumberOfSearchDomains {
2023-12-18 11:46:58 +01:00
// lets log all skipped Domains
2023-11-03 13:05:39 +01:00
log . Infof ( "already appended %d domains to search list. Skipping append of %s domain" , fileMaxNumberOfSearchDomains , sd )
continue
}
* s = append ( * s , sd )
}
2024-01-24 16:47:26 +01:00
2023-11-03 13:05:39 +01:00
return initialLineChars
2022-11-23 13:39:42 +01:00
}
func copyFile ( src , dest string ) error {
stats , err := os . Stat ( src )
if err != nil {
2024-01-30 09:58:56 +01:00
return fmt . Errorf ( "checking stats for %s file when copying it. Error: %s" , src , err )
2022-11-23 13:39:42 +01:00
}
bytesRead , err := os . ReadFile ( src )
if err != nil {
2024-01-30 09:58:56 +01:00
return fmt . Errorf ( "reading the file %s file for copy. Error: %s" , src , err )
2022-11-23 13:39:42 +01:00
}
err = os . WriteFile ( dest , bytesRead , stats . Mode ( ) )
if err != nil {
2024-01-30 09:58:56 +01:00
return fmt . Errorf ( "writing the destination file %s for copy. Error: %s" , dest , err )
2022-11-23 13:39:42 +01:00
}
return nil
}
2024-01-24 16:47:26 +01:00
func isContains ( subList [ ] string , list [ ] string ) bool {
for _ , sl := range subList {
var found bool
for _ , l := range list {
if sl == l {
found = true
}
}
if ! found {
return false
}
}
return true
}