diff --git a/README.md b/README.md index 8a717d2..5fadfe7 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ The following rules apply: * use any IP address provided via the X-Real-IP header e.g. when used behind HTTP reverse proxy such as nginx, or * use any IP address used by the connecting HTTP client +If you want to provide an additional IPv6 address as myip6 parameter the myip parameter containing an IPv4 address has to be present, too! No automatism is applied then. + ### SSL, multiple listen ports Use a webserver as a proxy to handle SSL and/or multiple listen addresses and ports. DynDNS.com provides HTTP on port 80 and 8245 and HTTPS on port 443. diff --git a/lib/dyndnsd.rb b/lib/dyndnsd.rb index c836aea..199b52a 100644 --- a/lib/dyndnsd.rb +++ b/lib/dyndnsd.rb @@ -78,24 +78,40 @@ module Dyndnsd return @responder.response_for_error(:host_forbidden) if not @users[user]['hosts'].include? hostname end - # fallback value, always present - myip = env["REMOTE_ADDR"] + myip = nil - # check whether X-Real-IP header has valid IPAddr - if env.has_key?("HTTP_X_REAL_IP") + if params.has_key?("myip6") + # require presence of myip parameter as valid IPAddr (v4) and valid myip6 + return @responder.response_for_error(:host_forbidden) if not params["myip"] begin - IPAddr.new(env["HTTP_X_REAL_IP"]) - myip = env["HTTP_X_REAL_IP"] + IPAddr.new(params["myip"], Socket::AF_INET) + IPAddr.new(params["myip6"], Socket::AF_INET6) + + # myip will be an array + myip = [params["myip"], params["myip6"]] rescue ArgumentError + return @responder.response_for_error(:host_forbidden) end - end + else + # fallback value, always present + myip = env["REMOTE_ADDR"] - # check whether myip parameter has valid IPAddr - if params.has_key?("myip") - begin - IPAddr.new(params["myip"]) - myip = params["myip"] - rescue ArgumentError + # check whether X-Real-IP header has valid IPAddr + if env.has_key?("HTTP_X_REAL_IP") + begin + IPAddr.new(env["HTTP_X_REAL_IP"]) + myip = env["HTTP_X_REAL_IP"] + rescue ArgumentError + end + end + + # check whether myip parameter has valid IPAddr + if params.has_key?("myip") + begin + IPAddr.new(params["myip"]) + myip = params["myip"] + rescue ArgumentError + end end end diff --git a/lib/dyndnsd/generator/bind.rb b/lib/dyndnsd/generator/bind.rb index 3d03e06..44b8f3d 100644 --- a/lib/dyndnsd/generator/bind.rb +++ b/lib/dyndnsd/generator/bind.rb @@ -18,11 +18,13 @@ module Dyndnsd out << "@ IN SOA #{@dns} #{@email_addr} ( #{zone['serial']} 3h 5m 1w 1h )" out << "@ IN NS #{@dns}" out << "" - zone['hosts'].each do |hostname,ip| - ip = IPAddr.new(ip).native - type = ip.ipv6? ? "AAAA" : "A" - name = hostname.chomp('.' + @domain) - out << "#{name} IN #{type} #{ip}" + zone['hosts'].each do |hostname,ips| + (ips.is_a?(Array) ? ips : [ips]).each do |ip| + ip = IPAddr.new(ip).native + type = ip.ipv6? ? "AAAA" : "A" + name = hostname.chomp('.' + @domain) + out << "#{name} IN #{type} #{ip}" + end end out << "" out << @additional_zone_content diff --git a/lib/dyndnsd/responder/dyndns_style.rb b/lib/dyndnsd/responder/dyndns_style.rb index 2bef220..106f75a 100644 --- a/lib/dyndnsd/responder/dyndns_style.rb +++ b/lib/dyndnsd/responder/dyndns_style.rb @@ -11,9 +11,9 @@ module Dyndnsd return [200, {"Content-Type" => "text/plain"}, ["nohost"]] if state == :host_forbidden return [200, {"Content-Type" => "text/plain"}, ["notfqdn"]] if state == :hostname_malformed end - + def response_for_changes(states, ip) - body = states.map { |state| "#{state} #{ip}" }.join("\n") + body = states.map { |state| "#{state} #{ip.is_a?(Array) ? ip.join(' ') : ip}" }.join("\n") return [200, {"Content-Type" => "text/plain"}, [body]] end end diff --git a/lib/dyndnsd/responder/rest_style.rb b/lib/dyndnsd/responder/rest_style.rb index f349a61..681e5e0 100644 --- a/lib/dyndnsd/responder/rest_style.rb +++ b/lib/dyndnsd/responder/rest_style.rb @@ -11,9 +11,9 @@ module Dyndnsd return [403, {"Content-Type" => "text/plain"}, ["Forbidden"]] if state == :host_forbidden return [422, {"Content-Type" => "text/plain"}, ["Hostname malformed"]] if state == :hostname_malformed end - + def response_for_changes(states, ip) - body = states.map { |state| state == :good ? "Changed to #{ip}" : "No change needed for #{ip}" }.join("\n") + body = states.map { |state| state == :good ? "Changed to #{ip.is_a?(Array) ? ip.join(' ') : ip}" : "No change needed for #{ip.is_a?(Array) ? ip.join(' ') : ip}" }.join("\n") return [200, {"Content-Type" => "text/plain"}, [body]] end end diff --git a/spec/daemon_spec.rb b/spec/daemon_spec.rb index 15db4ae..ba79ff5 100644 --- a/spec/daemon_spec.rb +++ b/spec/daemon_spec.rb @@ -170,8 +170,37 @@ describe Dyndnsd::Daemon do it 'uses clients remote IP address from X-Real-IP header if behind proxy' do authorize 'test', 'secret' + get '/nic/update?hostname=foo.example.org', '', 'HTTP_X_REAL_IP' => '10.0.0.1' expect(last_response).to be_ok expect(last_response.body).to eq('good 10.0.0.1') + + get '/nic/update?hostname=foo.example.org', '', 'HTTP_X_REAL_IP' => '2001:db8::1' + expect(last_response).to be_ok + expect(last_response.body).to eq('good 2001:db8::1') + end + + it 'supports an IPv4 and an IPv6 address in one request' do + authorize 'test', 'secret' + + get '/nic/update?hostname=foo.example.org&myip=1.2.3.4&myip6=2001:db8::1' + expect(last_response).to be_ok + expect(last_response.body).to eq("good 1.2.3.4 2001:db8::1") + + get '/nic/update?hostname=foo.example.org&myip=BROKENIP&myip6=2001:db8::1' + expect(last_response).to be_ok + expect(last_response.body).to eq('nohost') + + get '/nic/update?hostname=foo.example.org&myip=1.2.3.4&myip6=BROKENIP' + expect(last_response).to be_ok + expect(last_response.body).to eq('nohost') + + get '/nic/update?hostname=foo.example.org&myip6=2001:db8::10' + expect(last_response).to be_ok + expect(last_response.body).to eq('nohost') + + get '/nic/update?hostname=foo.example.org&myip=1.2.3.40' + expect(last_response).to be_ok + expect(last_response.body).to eq('good 1.2.3.40') end end