Inactive route when gateway not in same subnet, but has ARP

ip address/print 
Columns: ADDRESS, NETWORK, INTERFACE, VRF
# ADDRESS          NETWORK       INTERFACE  VRF 
0 172.16.240.2/27  172.16.240.0  ether1     main

And:

DST-ADDRESS      GATEWAY      ROUTING-TABLE  DISTANCE

0  Is 0.0.0.0/0        10.180.55.1  main                  1
1  As 10.180.55.1/32   ether1       main                  1
DAc 172.16.240.0/27  ether1       main                  0

arp=proxy-arp
on both routers.
10.180.55.1 is reachable and there's icmp ping.
Why the route is Inactive? ( same works on a linux machine )

i think you need to specify prefered source on default route in this case

/ip route add dst-address=0.0.0.0/0 gateway=10.180.55.1 pref-src=172.16.240.2

thanks but same.

hmmmm
using almost same as you for PtP

address
add address=XXX.YYY.41.240 interface=Vlan2000 network=0.2.0.240

route
add dst-address=0.0.0.0/0 gateway=0.2.0.240 pref-src=XXX.YYY.41.240

yes, this works.
But It seems there's no dhcp-option for pref-src.
The goal is to achieve the p2p using a custom DHCP server.

I have handled RFC3442:

package dhcpd

import (
	"net"
	"strings"
	"time"

	"codeberg.io/kloudstack/mink/internal/ipam"
	"github.com/spf13/viper"

	"github.com/insomniacslk/dhcp/dhcpv4"
	"github.com/insomniacslk/dhcp/dhcpv4/server4"
	"github.com/samber/lo"
	"k8s.io/klog/v2"
)

type HDHCPV4 struct {
	ipam   ipam.IPAM
	server server4.Server
}

func StartV4Server(ifaceName string) error {
	laddr := &net.UDPAddr{IP: net.IPv4zero, Port: 67}

	ipam := ipam.New()
	h := &HDHCPV4{ipam: *ipam}

	srv, err := server4.NewServer(ifaceName, laddr, h.handler, server4.WithDebugLogger())
	if err != nil {
		return err
	}
	return srv.Serve()
}

func (hd4 *HDHCPV4) handler(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) {
	macPrefix := viper.GetString("MAC_PREFIX")
	mac := req.ClientHWAddr.String()
	klog.V(4).Infof("MAC: %s , accepted prefix %s", mac, macPrefix)
	if !strings.HasPrefix(mac, macPrefix) {
		klog.V(4).Infof("MAC: %s does not match prefix %s", mac, macPrefix)
		return
	}

	klog.V(4).Infof("=== DHCP %s from %s ===", req.MessageType(), req.ClientHWAddr)
	klog.V(4).Infof("XID: %s CIAddr: %s SIAddr: %s", req.TransactionID, req.ClientIPAddr, req.ServerIPAddr)

	switch req.MessageType() {
	case dhcpv4.MessageTypeDiscover:
		hd4.handleDiscover(conn, peer, req)
	case dhcpv4.MessageTypeRequest:
		hd4.handleRequest(conn, peer, req)
	default:
		klog.Infof("Ignoring DHCP message: %s", req.MessageType())
	}
}

// ------------------------------------------------------------
//  SHARED HELPERS (DRY)
// ------------------------------------------------------------

// buildReply creates a reply packet (Offer or ACK)
func buildReply(req *dhcpv4.DHCPv4, msgType dhcpv4.MessageType) (*dhcpv4.DHCPv4, error) {
	reply, err := dhcpv4.NewReplyFromRequest(req)
	if err != nil {
		return nil, err
	}

	reply.UpdateOption(dhcpv4.OptMessageType(msgType))
	// Add lease time (required!)
	reply.UpdateOption(dhcpv4.OptIPAddressLeaseTime(3600 * time.Second))
	reply.UpdateOption(dhcpv4.OptRenewTimeValue(1800 * time.Second))
	reply.UpdateOption(dhcpv4.OptRebindingTimeValue(3150 * time.Second))

	return reply, nil
}

// applyCommonOptions sets mask, server ID, routes, etc.
func (hd4 *HDHCPV4) applyCommonOptions(pkt *dhcpv4.DHCPv4) {
	clusterIP, err := hd4.ipam.FindClusterIPbyFamilyandMAC(pkt.ClientHWAddr.String(), "v4")
	if err != nil {
		klog.Errorf("IPAM lookup failed: %v", err)
		return
	}
	clusterIPPool, err := hd4.ipam.FindClusterIPPoolByName(clusterIP.Spec.ClusterIPPool)
	if err != nil {
		klog.Errorf("%v", err)
		return
	}
	_, ipNet, _ := net.ParseCIDR(clusterIPPool.Spec.CIDR)
	pkt.UpdateOption(dhcpv4.OptSubnetMask(ipNet.Mask))
	ip := net.ParseIP(clusterIP.Spec.Address)
	pkt.YourIPAddr = ip

	if clusterIPPool.Spec.Gateway != "" {
		var routes []*dhcpv4.Route
		gwIP := net.ParseIP(clusterIPPool.Spec.Gateway)
		if !ipNet.Contains(gwIP) {
			p2pRoute := &dhcpv4.Route{
				Dest: &net.IPNet{
					IP:   gwIP,
					Mask: net.CIDRMask(32, 32),
				},
				Router: net.ParseIP("0.0.0.0"),
			}
			routes = append(routes, p2pRoute)
		}
		defaultRoute := &dhcpv4.Route{
			Dest: &net.IPNet{
				IP:   net.ParseIP("0.0.0.0"),
				Mask: net.CIDRMask(0, 32),
			},
			Router: gwIP,
		}
		routes = append(routes, defaultRoute)
		pkt.UpdateOption(dhcpv4.OptClasslessStaticRoute(routes...))
	}

	// Server IP
	serverIP := net.ParseIP(viper.GetString("SERVER"))
	pkt.ServerIPAddr = serverIP
	pkt.UpdateOption(dhcpv4.OptServerIdentifier(serverIP))

	dnsServers := lo.Map(strings.Split(viper.GetString("DNS"), ","), func(d string, index int) net.IP {
		return net.ParseIP(d)
	})

	pkt.UpdateOption(dhcpv4.OptDNS(dnsServers...))

}

// sendPacket writes the DHCP packet
func sendPacket(conn net.PacketConn, peer net.Addr, pkt *dhcpv4.DHCPv4) {
	_, err := conn.WriteTo(pkt.ToBytes(), peer)
	if err != nil {
		klog.Errorf("send failed: %v", err)
	}
}

// ------------------------------------------------------------
//              OFFER HANDLER
// ------------------------------------------------------------

func (hd4 *HDHCPV4) handleDiscover(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) {

	offer, err := buildReply(req, dhcpv4.MessageTypeOffer)
	if err != nil {
		klog.Errorf("Offer reply build failed: %v", err)
		return
	}

	hd4.applyCommonOptions(offer)

	klog.V(4).Infof("Sending OFFER → %s", offer.YourIPAddr)

	sendPacket(conn, peer, offer)
}

// ------------------------------------------------------------
//              REQUEST HANDLER
// ------------------------------------------------------------

func (hd4 *HDHCPV4) handleRequest(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) {

	ip := req.RequestedIPAddress()
	if ip == nil {
		klog.Errorf("Client REQUEST had no RequestedIPAddress")
		return
	}

	ack, err := buildReply(req, dhcpv4.MessageTypeAck)
	if err != nil {
		klog.Errorf("Ack reply build failed: %v", err)
		return
	}

	ack.YourIPAddr = ip

	hd4.applyCommonOptions(ack)

	klog.V(4).Infof("Sending ACK → %s", ack.YourIPAddr)

	sendPacket(conn, peer, ack)
}

maybe this ?
DHCP Option 121 (Classless Static Route Option) allows a DHCP server to push custom static routes directly to client devices

or, if you using own/custom dhcp server, you could send any custom option and then grab values from mikrotik dhcp-client script and use them from script to set-up network

edit: untested, only wild guessing

You need to adjust the scope and target-scope values of your manually added routes (the ones with the static s flag) for it to work. Use

/ip route print proplist=dst-address,gateway,immediate-gw,distance,scope,target-scope,routing-table 

or make the columns visible in WinBox to see the current scope and target-scope. If you don't explicitly set them when adding the routes, they will have the default values of 30 and 10, which means the gateway 10.180.55.1 of your default route will not resolve (target-scope (10) is smaller than the referenced route's scope (30)).

Add the two static routes like this instead:

/ip route 
add dst-address=10.180.55.1/32 gateway=ether1 routing-table=main scope=10 target-scope=5
add dst-address=0.0.0.0/0 gateway=10.180.55.1 routing-table=main scope=30 target-scope=10

As you can see, now the route needed to resolve 10.180.55.1 can be used, because it has a scope of 10 (instead of 30) which is less than or equal to the target-scope=10 specified in the 2nd route. See also: Simple recursive failover for bears of little brain.

No need to set pref-src or to add the dummy address to the interface. The reason that it works when you added the dummy /32 address to the interface is because doing so dynamically adds the connected route (D and c flags) with the correct scope=10 target-scope=5 for you.

The trick is the network part of ip address.
When set network ip then ros creates a connected route for that. Then other depend routes get active.
It seems it's a bug.

I'm using option 121 and linux DHCP clients accepts it, but ROS's not.
I need more investigate and will aware you about results.

Thanks a lot anyway.

well, i am using this way, to preserve PUB addresses, because wasting 2 addr from /29 is shame

but tnx to @CGGXANNX , it is properly explained now

Thanks for the reply but there's no eq dhcp options for scope and target-scope.
The goal to set on-link route and default gateway using a DHCP server.
It's working using a linux dhcp client but not with routerOS dhcp client.

If DHCP is your only available option, you can use a workaround and just manually add a static

/ip route 
add dst-address=10.0.0.0/8 gateway=ether1 routing-table=main scope=10 target-scope=5

on the MikroTik device(s) once. The DHCP server can then announce default routes with any gateway address in that 10.0.0.0/8 range.

It will not cause conflict if the MikroTik router has other LAN/VLAN subnets in the 10.x.y.z range, because those subnets will usually install routes with bigger prefix length, and those routes will take precedence over the manually added /8 one.