2023-06-12 14:43:55 +02:00
//go:build !android
2022-11-23 13:39:42 +01:00
package dns
import (
2023-11-03 13:05:39 +01:00
"bufio"
2022-11-23 13:39:42 +01:00
"bytes"
"fmt"
"os"
2023-11-03 13:05:39 +01:00
"strings"
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
type fileConfigurator struct {
originalPerms os . FileMode
}
func newFileConfigurator ( ) ( hostManager , error ) {
return & fileConfigurator { } , nil
}
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 {
2022-11-23 13:39:42 +01:00
backupFileExist := false
_ , err := os . Stat ( fileDefaultResolvConfBackupLocation )
if err == nil {
backupFileExist = true
}
2023-12-18 11:46:58 +01:00
if ! config . RouteAll {
2022-11-23 13:39:42 +01:00
if backupFileExist {
err = f . restore ( )
if err != nil {
return fmt . Errorf ( "unable to configure DNS for this peer using file manager without a Primary nameserver group. Restoring the original file return err: %s" , err )
}
}
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 {
err = f . backup ( )
if err != nil {
return fmt . Errorf ( "unable to backup the resolv.conf file" )
2022-11-23 13:39:42 +01:00
}
}
2023-08-01 17:45:44 +02:00
2023-11-03 13:05:39 +01:00
searchDomainList := searchDomains ( config )
originalSearchDomains , nameServers , others , err := originalDNSConfigs ( fileDefaultResolvConfBackupLocation )
2023-08-01 17:45:44 +02:00
if err != nil {
2023-11-03 13:05:39 +01:00
log . Error ( err )
2023-08-01 17:45:44 +02:00
}
2023-11-03 13:05:39 +01:00
searchDomainList = mergeSearchDomains ( searchDomainList , originalSearchDomains )
buf := prepareResolvConfContent (
searchDomainList ,
2023-12-18 11:46:58 +01:00
append ( [ ] string { config . ServerIP } , nameServers ... ) ,
2023-11-03 13:05:39 +01:00
others )
log . Debugf ( "creating managed file %s" , defaultResolvConfPath )
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 )
}
2023-11-03 13:05:39 +01:00
return fmt . Errorf ( "got an creating resolver file %s. Error: %s" , defaultResolvConfPath , err )
2022-11-23 13:39:42 +01:00
}
2023-11-03 13:05:39 +01:00
log . Infof ( "created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s" , defaultResolvConfPath , len ( searchDomainList ) , searchDomainList )
2022-11-23 13:39:42 +01:00
return nil
}
func ( f * fileConfigurator ) restoreHostDNS ( ) error {
return f . restore ( )
}
func ( f * fileConfigurator ) backup ( ) error {
stats , err := os . Stat ( defaultResolvConfPath )
if err != nil {
return fmt . Errorf ( "got an error while checking stats for %s file. Error: %s" , defaultResolvConfPath , err )
}
f . originalPerms = stats . Mode ( )
err = copyFile ( defaultResolvConfPath , fileDefaultResolvConfBackupLocation )
if err != nil {
return fmt . Errorf ( "got error while backing up the %s file. Error: %s" , defaultResolvConfPath , err )
}
return nil
}
func ( f * fileConfigurator ) restore ( ) error {
err := copyFile ( fileDefaultResolvConfBackupLocation , defaultResolvConfPath )
if err != nil {
return fmt . Errorf ( "got error while restoring the %s file from %s. Error: %s" , defaultResolvConfPath , fileDefaultResolvConfBackupLocation , err )
}
return os . RemoveAll ( fileDefaultResolvConfBackupLocation )
}
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
}
func originalDNSConfigs ( resolvconfFile string ) ( searchDomains , nameServers , others [ ] string , err error ) {
file , err := os . Open ( resolvconfFile )
2022-11-23 13:39:42 +01:00
if err != nil {
2023-11-03 13:05:39 +01:00
err = fmt . Errorf ( ` could not read existing resolv.conf ` )
return
2022-11-23 13:39:42 +01:00
}
2023-11-03 13:05:39 +01:00
defer file . Close ( )
reader := bufio . NewReader ( file )
for {
lineBytes , isPrefix , readErr := reader . ReadLine ( )
if readErr != nil {
break
}
if isPrefix {
err = fmt . Errorf ( ` resolv.conf line too long ` )
return
}
line := strings . TrimSpace ( string ( lineBytes ) )
if strings . HasPrefix ( line , "#" ) {
continue
}
if strings . HasPrefix ( line , "domain" ) {
continue
}
if strings . HasPrefix ( line , "options" ) && strings . Contains ( line , "rotate" ) {
line = strings . ReplaceAll ( line , "rotate" , "" )
splitLines := strings . Fields ( line )
if len ( splitLines ) == 1 {
continue
}
line = strings . Join ( splitLines , " " )
}
if strings . HasPrefix ( line , "search" ) {
splitLines := strings . Fields ( line )
if len ( splitLines ) < 2 {
continue
}
searchDomains = splitLines [ 1 : ]
continue
}
if strings . HasPrefix ( line , "nameserver" ) {
splitLines := strings . Fields ( line )
if len ( splitLines ) != 2 {
continue
}
nameServers = append ( nameServers , splitLines [ 1 ] )
continue
}
others = append ( others , line )
}
return
}
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 {
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 )
}
return initialLineChars
2022-11-23 13:39:42 +01:00
}
func copyFile ( src , dest string ) error {
stats , err := os . Stat ( src )
if err != nil {
return fmt . Errorf ( "got an error while checking stats for %s file when copying it. Error: %s" , src , err )
}
bytesRead , err := os . ReadFile ( src )
if err != nil {
return fmt . Errorf ( "got an error while reading the file %s file for copy. Error: %s" , src , err )
}
err = os . WriteFile ( dest , bytesRead , stats . Mode ( ) )
if err != nil {
return fmt . Errorf ( "got an writing the destination file %s for copy. Error: %s" , dest , err )
}
return nil
}