Dead reckoning does it for you - most network games use the technique to simulate smooth motion. You can tell right away when a game de-syncs or lags, you get the rubber band effect.
I should have explained it better. The basic process is:
Client pushes shoot button. This command is sent to the server, guarantee delivery if you want. At this point, the client can spawn an object to illustrate immediate feedback, but this object is only on the client and may be short lived depending on how you spawn.
The server receives the command and processes it. If you have bullets that need to be spawned and move through your world, the server now performs this action. If it were me, I would only “spawn” the data for the bullet: position, directions, etc. and sent that to the clients, letting the clients spawn local objects that are only tracking positional information. This will help with smoothing motion.
The clients received the spawn bullet command and do so.
At this point, a bullet is shot. Now it’s up to the server to maintain the logic of the bullet - for example, move it, did it hit something, etc. Clients never need to worry about this, unless you want to predetermine what is happening on the client prior to receiving the command from the server.
Now, clients, to keep things smooth, use dead reckoning to move the object. Since you know the last know valid position of a bullet from the server (which can send periodic updates of positioning), and you know the direction the bullet was traveling, you can determine the most current position of the bullet and move it there on the client machine. This is not the object the server spawned - its the client version of the object that you are moving. If you spawn an actor on the server, then let the replication system move the object, otherwise you get jitter and rubber banding.
To smooth it more, between updates to positions received from the server, you interpolate based on fps, network packet speed, etc.